Приветствую {% habrauser %}!
Так уж сложилось, что рано или поздно каждый из web-разработчиков выпускающий свои проекты на не безызвестном Django Framework приходит к решению, что с родной Django-админкой надо что-то делать, ибо оная не самое привлекательное и функциональное решение под конечного пользователя. Да и рукодельничать надоедает в виде кастомных TreeSelectorWidget и т.п костылями.

После недолго гугления на тему сторонних оболочек для кастомизации django-админки у нас на руках имеется пара вполне вменяемых решений django-grappelli и django-admin-tools, о котором уже имеются повествования на хабре Улучшаем админку, а так же о способе как подружить admin-tools с Twitter Bootstrap Django Admin Bootstrap Theme и еще множество как визуальных, как и чисто функциональных решений…

И вот буквально вчера один из коллег мне подкинул ссылку на новенькую админку основанной на Twitter Bootstrap, о которой как не странно нет ни слова на хабре.

Для начала хочу привести примеры интерфесов всех описанных выше админок начиная от самой родной и заканчивая самой человечной на мой взгляд:
Родная админка

Grappelly


admin-tools

django-suit


А теперь к делу:
Установка и настройка django-suit сводится к самой установке и добавлением трех строчек в settings.py

    pip install django-suit

settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.request',
)

INSTALLED_APPS = (
    'suit',
    # ....
    'django.contrib.admin',
)


Это минимум, что от Вас требуется чтобы начать получать удовольствие от новой админки, но этого нам явно мало…

Создадим пару моделей над которыми будем издеваться экспериментировать дальше
models.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from django.db import models


class Category(models.Model):
    title = models.CharField(max_length=128)

    seo_keywords = models.CharField(max_length=256, blank=True, null=True)
    seo_description = models.CharField(max_length=256, blank=True, null=True)

    parent = models.ForeignKey('self', blank=True, null=True, related_name='categories')

    preview_text = models.TextField(max_length=512, blank=True, null=True, default='')
    detail_text = models.TextField(max_length=10*1024, blank=True, null=True, default='')

    preview_image = models.ImageField(blank=True, null=True, upload_to='images')
    detail_image = models.ImageField(blank=True, null=True, upload_to='images')

    active = models.BooleanField(default=True)

    def __unicode__(self):
        return self.title

    class Meta:
        verbose_name = u'Категория товаров'
        verbose_name_plural = u'Категории товаров'


class Product(models.Model):
    title = models.CharField(max_length=128)

    seo_keywords = models.CharField(max_length=256, blank=True, null=True)
    seo_description = models.CharField(max_length=256, blank=True, null=True)

    parent = models.ForeignKey(Category, blank=True, null=True, related_name='products')

    preview_text = models.TextField(max_length=512, blank=True, null=True)
    detail_text = models.TextField(max_length=10*1024, blank=True, null=True)

    preview_image = models.ImageField(blank=True, null=True, upload_to='images')
    detail_image = models.ImageField(blank=True, null=True, upload_to='images')

    price = models.FloatField(blank=True, null=True, default=0.0)
    sale = models.FloatField(blank=True, null=True, default=0.0)

    active = models.BooleanField(default=True)

    def __unicode__(self):
        return self.title

    class Meta:
        verbose_name = u'Товар'
        verbose_name_plural = u'Товары'


admin.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from django import forms

from django.contrib import admin
from django.contrib.admin import ModelAdmin, TabularInline
from models import Category, Product, ProductSliderImage


class AdminCategory(ModelAdmin):
    search_fields = ['title']
    list_display = ['title', 'active']
    list_filter = ('active',)
    

class AdminProduct(ModelAdmin):
    search_fields = ['title']
    list_display = ['title', 'active']
    list_filter = ('active', 'parent')


admin.site.register(Category, AdminCategory)
admin.site.register(Product, AdminProduct)


и получаем красивое плоское дерево категорий

и не менее красивую и плоскую страницу редактирования


Первым делом я решил преобразить страницу редактирования поместив смысловые блоки в табы. Для этото приложим совсем немного усилий и подправим регистрацию моделей
admin.py
class AdminCategory(ModelAdmin):
    search_fields = ['title']
    list_display = ['title', 'active']
    list_filter = ('active',)

    fieldsets = (
        (None, {
            'classes': ('suit-tab suit-tab-general',),
            'fields': (('title', 'active'), 'parent')
        }),
        ('Preview', {
            'classes': ('suit-tab suit-tab-preview',),
            'fields': ('preview_image', 'preview_text')
        }),
        ('Detail', {
            'classes': ('suit-tab suit-tab-detail',),
            'fields': ('detail_image', 'detail_text')
        }),
        ('Seo', {
            'classes': ('suit-tab suit-tab-seo',),
            'fields': ('seo_keywords', 'seo_description')
        }),
    )

    suit_form_tabs = (('general', 'General'), ('preview', 'Preview'), ('detail', 'Detail'), ('seo', 'Seo'),)



После чего захотелось мне сделать дерево более наглядным с помошью django-mptt
Редактируем наши модели и мигрируемся:
(оставил только изменения, остальное опустил)
#!/usr/bin/python
# -*- coding: utf-8 -*-
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey


class Category(MPTTModel):
    ....
    parent = TreeForeignKey('self', blank=True, null=True, related_name='categories')
    ....
    
class Product(models.Model)
    ....
    parent = TreeForeignKey(Category, blank=True, null=True, related_name='products')
    ...


admin.py

from mptt.admin import MPTTModelAdmin


class AdminCategory(MPTTModelAdmin):
    ....

так-то лучше, но и это не предел.


Теперь займемся самым сложным — прикручиванием wysiwyg-редактора CKEdiror
    pip install django-suit-ckeditor
    ./manage.py collectstatic --noinout

и поправим код:
settings.py
INSTALLED_APPS = (
    'suit',
    'suit_ckeditor',
    # ....
    'django.contrib.admin',
)

admin.py
from django.db import models
from suit_ckeditor.widgets import CKEditorWidget


class AdminCategory(MPTTModelAdmin):
    formfield_overrides = {
        models.TextField: {'widget': CKEditorWidget},
    }
    ...


class AdminProduct(ModelAdmin):
    formfield_overrides = {
        models.TextField: {'widget': CKEditorWidget},
    }
    ...



С этим все понятно скажете Вы, а как же кастомные виджеты из admin-tools от них придется отказаться?
А вот и нет, отвечу я.
редактируем наш settings.py и дописываем следующее:
# Django Suit configuration example
SUIT_CONFIG = {
    "MENU": (
        {'label': "Example", "icon": 'icon-ok', "models": [
            {"label": "for all", 'url': '/example/'},
            {"label": "for stuff", 'url': '/admin/example/'},
        ]},
    )
}

после чего наше меню изменится и будет выглядеть следующим образом

теперь создадим вьюшку и пропишем 2 url один будет доступен всем желаюшим другой только тем у кого есть доступ в админку
views.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from django.views.generic.base import TemplateView


class ExampleView(TemplateView):
    template_name = 'example.html'

    def get_context_data(self, **kwargs):
        kwargs['title'] = "Привет Хабр!"
        return super(ExampleView, self).get_context_data(**kwargs)

example.html
{% extends 'admin/base.html' %}
{% block content %}
        <h1>Привет Habrahabr.ru</h1>
{% endblock %}

и наконец urls.py
urlpatterns = patterns('',
    # Examples:
    url(r'^example/$', ExampleView.as_view()),
    url(r'^admin/example/$', ExampleView.as_view()),
    ....
    )



Вот так легко и не принудженно за каких-то 30 минут админка преображается до неузнаваемости.
Всем кому стало интересно посмотреть руками максимум возможнотей этой замечательнйо на мой взгляд альтернативе всех предидущих админок: Wellcome на сайт разработчиков


P.S: Сильно не пинайте — это мой первый пост на хабре.