django-controlcenter

    django-controlcenter

    Всем привет, хочу поделиться своей небольшой разработкой — django-controlcenter. Это приложение для создания дешбоардов для вашего django-проекта.

    Цель


    Django-admin — отличный пример CRUD и невероятно полезное приложение. Вы подключаете модель, а затем видите табличку со всеми записями в базе. Потом вносите вторую, а затем третью и так далее. Со временем у вас набегает много таких табличек: с заказами, комментами, запросами, отзывами — и вы начинаете бегать туда-сюда между всеми ними по несколько раз на дню. А еще иногда хочется всяких графиков.

    Django-controlcenter появился как раз из-за подобной ситуации, когда требовалось регулярно проверять несколько моделей на новые записи и игнорировать их, или изменять, или удалять, и видеть динамику в графиках.

    Дисклеймер


    Текущая версия не использует ajax, и по сути это даже не CRUD, это только Read, но с расширенными возможностями.

    Простой пример


    Давайте начнем с небольшого примера:

    # project/dashboard.py
    
    from controlcenter import Dashboard, widgets
    from project.app.models import Model
    
    class ModelItemList(widgets.ItemList):
        model = Model
        list_display = ['pk', 'field']
    
    class MyDashboard(Dashboard):
        widgets = (
            ModelItemList,
        )
    
    # project/settings.py
    CONTROLCENTER_DASHBOARDS = [
        'project.dashboards.MyDashboard'
    ]

    Этот виджет выведет табличку в две колонки с 10 последними значениями (по-умолчанияю ItemList ограничен в выдаче, чтобы не порвать вам страницу).

    itemlist

    Я использовал знакомые термины; в целом, виджет — это смесь Views и ModelAdmin в плане именования методов и атрибутов, и их поведения.

    class ModelItemList(widgets.ItemList):
        model = Model
        queryset = model.active_objects.all()
        list_display = ('pk', 'field', 'get_foo')
        list_display_links = ('field', 'get_foo')
        template_name = 'my_custom_template.html'
    
        def get_foo(self, obj):
            return 'foo'
        get_foo.allow_tags = True
        get_foo.short_description = 'Foo!'

    Как видите, ничего нового. Пока еще.

    Дисклеймер


    Дальше пойдет по сути документация, так что, если вам удобнее разбирать примеры, переходите сразу к ним.

    Виджеты


    Основных виджета всего три: Widget, ItemList и Chart. Еще есть Group, но это не виджет, а обертка. Начнем с него.

    Group


    Виджеты могут собираться в группы, тогда они будут переключаться по клику по заголовоку. Для группировки виджеты указываются списком/картежом или используется специальная обертка — Group.

    class MyDashboard(Dashboard):
        widgets = (
            Foo,
            (Bar, Baz),
            Group((Egg, Spam), width=widgets.LARGE, height=300,
                  attrs={'class': 'my_class', 'data-foo': 'foo'}),
        )

    Group принимает три необязательных аргумента: width, height, attrs.
    Важный момент: такой "составной" виджет получает высоту самого "высокого" в группе, поскольку, дизайн адаптивный и использует Masonry — если не зафиксировать габариты блока, есть шанс получить забавный эффект, когда переключаясь между виджетами группы у вас будет перестраиваться весь дешбоард.

    Group.width


    Сетка дешбоарда адаптивна: до 768px виджеты занимают всю ширину, затем 50% или 100%. От 1000px используется 6-колонная сетка. Для удобства, значения хранятся в модуле widgets:

    # controlcenter/widgets.py
    MEDIUM = 2   # 33%  или  [x] + [x] + [x]
    LARGE = 3    # 50%  или  [  x ] + [ x  ]
    LARGER = 4   # 66%  или  [    x  ] + [x]
    LARGEST = 6  # 100% или  [      x      ]

    Промежуточные значения не особо полезны, но использовать их никто не запрещает.

    Group.height


    Изначально None, но получив интеджер, выставит виджету это значение как max-height и появится необязательный скролл.
    width и height есть и у виджетов, в случае, если эти значения не указаны в Group, берется максимальное значение у виджетов в этой группе.

    Group.attrs


    Все, что захочется вписать в виджет как html атрибут. Можно даже задать id.

    Widget


    Базовый виджет. Практически ничего не умеет. Но обладает одной полезностью: в момент создания оборачивает метод valuesseries, labels, legend для чартов) в дескриптор cached_property. Соответственно, значения доступны как при обращению к атрибуту (без вызова), а данные кешируются. Это просто небольшое удобство, поскольку приходится часто обращаться к этим методам. Например, для чартов делается такая штука:

    def labels(self):
        return [x for x, y in self.values]
    
    def series(self):
        return [y for x, y in self.values]
    
    def values(self):
        return self.get_queryset().values_list('label', 'series')

    Еще с десяток раз это спросится в шаблонах, так что лучше сразу все закешировать.

    Widget.title


    Заголовок виджета. Если не задан, сформируется из названия класса.

    Widget.width и Widget.height


    Поведение аналогичное Group (см. выше).

    Widget.model


    Принимает django.db.models.Model.

    Widget.get_queryset


    Поведение анологичное у django.generic.views:

    • если есть queryset, вернет его.
    • если есть model, вернет его дефолтного менеджера.

    Widget.values и Widget.limit_to


    Вызывает get_queryset.
    Поэтому, если у вас данные "где-то там", переписываем этот метод и забываем про get_queryset. Хоть из файла читайте. Также ограничивает кверисет по значению limit_to, если оно не равно None, вот так: self.get_queryset()[:self.limit_to].

    Widget.template_name_prefix


    Директория с темплейтами.

    Widget.template_name


    Имя темплейта.

    Widget.get_template_name


    Возвращает Widget.template_name_prefix + Widget.template_name.

    ItemList


    Это самый сложный виджет и одновременно самый простой. Простой, потому что жует все, что не поподя: модели, словари, листы, namedtuple — все, что поддается итерации или имеет доступ по ключу/атрибуту. Однако, есть особенности.

    class ModelItemList(widgets.ItemList):
        model = Model
        queryset = model.objects.all()
        list_display = ['pk', 'field']

    ItemList.list_display


    Во время рендеринга шаблонов значения из элементов в values берутся по ключам из list_display (для моделей, словарей и namedtuple), для последовательностей индекс ключа равен индексу значения, грубо говоря zip(list_display, values).

    Нумерация строк

    Добавьте # в list_display и получите нумерацию строк. Также "решетку" можно заменить на другой символ установив его в качестве значения в settings.CONTROLCENTER_SHARP.

    ItemList.list_display_links


    Поведение аналогичное list_display_links в django.

    Ссылка на редактирование объекта


    ItemList пытается повесить ссылку на страницу редактирования объекта в админке, для этого ему нужен класс объекта и первичный ключ. Поэтому виджет будет искать эти данные везде: если values вернет инстанс модели, то вытянет все из него. Если values вернет словарь, список или namedtuple, то понадобится указать ItemList.model, потому что, понятно, больше не откуда. Во всех случаях виджет попытается найти pk или id самостоятельно, но в случае последовательностей это сделать не получится, поэтому виджет будет искать эти ключи в list_display сопоставляя его индекс с индексом значений последовательности.
    Кстати, виджет понимает deferred модели, так что можно писать так: queryset = Model.obejcts.defer('field').
    Для работы этой фичи модель должна быть зарегистрирована в django-admin.

    Ссылка на changelist модели


    Иногда недостаточно посмотреть на 10 последнийх значений и надо перейти на страницу модели. ModelAdmin строит такие пути самостоятельно. Но в виджет можно подставить все, что угодно в queryset, поэтому придется помочь. Вариантов несколько:

    class ModelItemList(widgets.ItemList):
        model = Model
        # Ссылка на модель
        changelist_url = model
    
        # То же самое, но с фильтром и сортировкой
        changelist_url = model, {'status__exact': 0, 'o': '-7.-1'}
    
        # То же самое со строкой
        changelist_url = model, 'status__exact=0&o=-7.-1'
    
        # Или так
        changelist_url = '/admin/model/'
        changelist_url = 'http://www.yandex.ru'

    Для работы этой фичи модель должна быть зарегистрирована в django-admin.

    ItemList.sortable


    Для того, чтобы сортировать табличку, достаточно указать sortable=True, но помните, что джанга сортирует в базе, а виджет на стороне клиента, поэтому могут случаться казусы, например, если в столбце даты указаны в формате dd.mm. Используется библиотека sortable.js.

    ItemList.method.allow_tags и ItemList.method.short_description


    Поведение аналогичное джанговским allow_tags и short_description.

    ItemList.empty_message


    Выведет это значение, если values вернет пустой список.

    ItemList.limit_to


    По-умолчанию имеет значение 10, чтобы вы себе в ногу не выстрелили.

    Chart


    Для графиков используется Chartist — это небольшая библиотека… со своими особенностями. Она очень быстрая, просто мгновенная, я просто не мог пройти мимо.

    Есть три типа чартов: LINE, BAR, PIE; и соответствующие к ним классы: LineChart, BarChart, PieChart. Плюс несколько дополнительных, об этом позже.

    Chart определяет три дополнительных метода: legend, lables, series, которые еще и кешируются. Все три метода должны возвращать json-сериализуемый объект, к коим не относятся генераторы.

    class MyChart(widgets.Chart):
        def legend(self):
            return []
    
        def labels(self):
            return []
    
        def series(self):
            return []

    Chart.legend


    Из коробки Chartist не умеет показывать легенду, но без нее никак, поскольку чартист еще и не рисует значения на графике (да, есть такой момент). Легенда поможет в таких случаях.

    Chart.labels


    Значения на оси x. Должен возвращать последовательность, ни в коем случае не передавайте генератор.

    Chart.series


    Значения на оси y. Должен возвращать список списков, поскольку на графиках могут быть множественные данные. Опять же, никаких генераторов. Тут есть небольшая "готча", для типа BAR с одним типом значений передается "плоский" список, т.е. не вложенный, при этом устанавливается дополнительная опция для чартиста. Проще всего использовать SingleBarChart — в нем все настроено.

    Chart.Chartist


    Chart — это виджет с дополнительным классом Chartist внутри на манер Meta или Media в джанге.

    class MyChart(Chart):
        class Chartist:
            klass = widgets.LINE
            point_lables = True
            options = {
                'reverseData': True,
                'axisY': {
                    'onlyInteger': True,
                },
                'fullWidth': True,
            }

    С той лишь разницей, что при использовании Chartist не нужно наследовать родительский класс, т.е. это как бы не классический python inheritance: вы пишете class Chartist:, а не class Chartist(Parent.Chartist): — поля наследуются автоматически. В наследующем классе переписываются все поля, кроме options, который склеивается с родительским, т.е. в дочернем классе можно написать только новые пары ключ/значение, а не Parent.Chartist.options.copy().update({'foo': 'bar'}). Конечно, у этого метода есть и обратная сторона: дефолтные значения, при необходимости, придется переписать.

    Важно! Для LineChart установлено 'reverseData': True, которое реверсирует значения labels и series на клиенте. Чаще всего этот тип чартов используется для отображения последних данных и, чтобы вам не пришлось в каждом первом чарте этим заниматься вручную, эта опция включена по-умолчанию.

    Chart.Chartist.klass

    Определяет тип чарта: widgets.LINE, widgets.BAR, widgets.PIE.

    Chart.Chartist.point_lables

    Подлючается плугин к Chartist, который проставляет значения на графике. Это странно, но дефолтный чартист обходится без значений на самом графике. К сожалению, эта штука работает только с widgets.LINE. В остальных случаях поможет метод legend.

    Chart.Chartist.options

    Словарь, который целиком отправляется в джсон и передается конструктуру чартиста. Все опции описаны на сайте.

    Дополнительные классы


    В модуле widgets подготовлены еще несколько вспомогательных классов: SingleLineChart, SingleBarChart, SinglePieChart — для простых юзкейсов.

    class BlogsChart(widgets.SingleBarChart):
        model = Blog
        values_list = ('name', 'score')

    Ну, собсно, и все. Значения name пойдут в ось x, а score в ось y.

    Dashboard


    Приложение поддерживает до 10 "панелей", которые доступны по адресу: /admin/dashboards/[pk]/ — где pk индекс в списке settings.CONTROLCENTER_DASHBOARDS.

    Dashboard.widgets


    Принимает список виджетов.

    Dashboard.title


    Произвольный заголовок. Если не задан, будет сформирован из названия класса.

    Dashboard.Media


    Класс Media из джанги.

    Настройки


    #  Список дешбоардов
    CONTROLCENTER_DASHBOARDS = []
    
    # Диез для нумерации строк в `ItemList`
    CONTROLCENTER_SHARP = '#'
    
    # Цвета для графиков. Используются дефолтные для `Chartist`,
    # но еще я подготовил тему в цветах `Material Design`,
    # подстваляем `material`.
    CONTROLCENTER_CHARTIST_COLORS = 'default'

    Примеры!


    Давайте сделаем все то же самое, что и на скриншоте.
    Создадим проект, назовем его pizzeria, добавим в него приложение pizza.

    pizzeria.pizza.models


    from __future__ import unicode_literals
    from django.db import models
    
    class Pizza(models.Model):
        name = models.CharField(max_length=100, unique=True)
    
        def __str__(self):
            return self.name
    
    class Restaurant(models.Model):
        name = models.CharField(max_length=100, unique=True)
        menu = models.ManyToManyField(Pizza, related_name='restaurants')
    
        def __str__(self):
            return self.name
    
    class Order(models.Model):
        created = models.DateTimeField(auto_now_add=True)
        restaurant = models.ForeignKey(Restaurant, related_name='orders')
        pizza = models.ForeignKey(Pizza, related_name='orders')

    Установка


    pip install django-controlcenter

    Внесем приложения в pizzeria.settings

    INSTALLED_APPS = (
        ...
        'controlcenter',
        'pizza',
    )
    
    # Забегая вперед
    CONTROLCENTER_DASHBOARDS = (
        'pizzeria.dashboards.MyDashboard'
    )

    Добавим урлы в pizzeria.urls

    from django.conf.urls import url
    from django.contrib import admin
    from controlcenter.views import controlcenter
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^admin/dashboard/', controlcenter.urls),
    ]

    Виджеты


    В файле pizzeria.dashboards создадим виджеты:

    import datetime
    from collections import defaultdict
    
    from controlcenter import app_settings, Dashboard, widgets
    from controlcenter.widgets.core import WidgetMeta
    from django.db.models import Count
    from django.utils import timezone
    from django.utils.timesince import timesince
    
    from .pizza.models import Order, Pizza, Restaurant
    
    class MenuWidget(widgets.ItemList):
        # Этот виджет отображает список пицц, которые были
        # проданы в конкретном ресторане. Мы будем его использовать
        # как базовый, а позже размножим для всех ресторанов.
        model = Pizza
        list_display = ['name', 'ocount']
        list_display_links = ['name']
    
        # По-умолчанию, в ItemList выборка ограничена, 
        # чтобы вы случайно не вывели всю таблицу в маленькой рамочке.
        limit_to = None
    
        # Если виджет будет больше 300, появится скролл
        height = 300
    
        def get_queryset(self):
            # Возвращает список пицц и подсчитывает заказы на сегодня
            restaurant = super(MenuWidget, self).get_queryset().get()
            today = timezone.now().date()
            return (restaurant.menu
                              .filter(orders__created__gte=today)
                              .order_by('-ocount')
                              .annotate(ocount=Count('orders')))
    
    class LatestOrdersWidget(widgets.ItemList):
        # Виджет отображает последние 20 заказов
        # в конкретном ресторане
        model = Order
        queryset = (model.objects
                         .select_related('pizza')
                         .filter(created__gte=timezone.now().date())
                         .order_by('pk'))
        # Добавим `#` чтобы разнумеровать список
        list_display = [app_settings.SHARP, 'pk', 'pizza', 'ago']
        list_display_links = ['pk']
    
        # Включим сортировку и выведем заголовки в таблице
        sortable = True
    
        # Отобразим последние 20
        limit_to = 20
    
        # Ограничим виджет по высоте
        height = 300
    
        # Дату красивенько
        def ago(self, obj):
            return timesince(obj.created)
    
    RESTAURANTS = [
        'Mama',
        'Ciao',
        'Sicilia',
    ]
    
    # Используем мета-класс, чтобы построить виджеты.
    # Можно, конечно, наследовать первый виджет и ручками определить классы.
    # Напомню, конструктор принимает следующие аргументы:
    # имя класса, наследуемые классы, атрибуты
    menu_widgets = [WidgetMeta('{}MenuWidget'.format(name),
                               (MenuWidget,),
                               {'queryset': Restaurant.objects.filter(name=name),
                                # Произвольный заголовок
                                'title': name + ' menu',
                                # Ссылка на `changelist` модели с GET параметром
                                'changelist_url': (
                                     Pizza, {'restaurants__name__exact': name})})
                    for name in RESTAURANTS]
    
    latest_orders_widget = [WidgetMeta(
                               '{}LatestOrders'.format(name),
                               (LatestOrdersWidget,),
                               {'queryset': (LatestOrdersWidget
                                                .queryset
                                                .filter(restaurant__name=name)),
                                'title': name + ' orders',
                                'changelist_url': (
                                     Order, {'restaurant__name__exact': name})})
                            for name in RESTAURANTS]
    
    class RestaurantSingleBarChart(widgets.SingleBarChart):
        # Строит бар-чарт по числу заказов
        title = 'Most popular restaurant'
        model = Restaurant
    
        class Chartist:
            options = {
                # По-умолчанию, Chartist может использовать
                # float как промежуточные значения, это ни к чему
                'onlyInteger': True,
                # Внутренние отступы чарта -- косметика
                'chartPadding': {
                    'top': 24,
                    'right': 0,
                    'bottom': 0,
                    'left': 0,
                }
            }
    
        def legend(self):
            # Выводит в легенде значения оси `y`,
            # поскольку, Chartist не рисует сами значения на графике
            return self.series
    
        def values(self):
            queryset = self.get_queryset()
            return (queryset.values_list('name')
                            .annotate(baked=Count('orders'))
                            .order_by('-baked')[:self.limit_to])
    
    class PizzaSingleBarChart(RestaurantSingleBarChart):
        # Наследует предыдущий виджет, поскольку,
        # нам нужны те же настройки, кроме типа чарта
        model = Pizza
        limit_to = 3
        title = 'Most popular pizza'
    
        class Chartist:
            # Заменяет тип чарта
            klass = widgets.PIE
    
    class OrderLineChart(widgets.LineChart):
        # Отображает динамику продаж в ресторанах
        # за последние 7 дней
        title = 'Orders this week'
        model = Order
        limit_to = 7
        # Зададим размерчик побольше
        width = widgets.LARGER
    
        class Chartist:
            # Настройки чартиста -- косметика
            options = {
                'axisX': {
                    'labelOffset': {
                        'x': -24,
                        'y': 0
                    },
                },
                'chartPadding': {
                    'top': 24,
                    'right': 24,
                }
            }
    
        def legend(self):
            # В легенду пойдут названия ресторанов
            return RESTAURANTS
    
        def labels(self):
            # По оси `x` дни
            today = timezone.now().date()
            labels = [(today - datetime.timedelta(days=x)).strftime('%d.%m')
                      for x in range(self.limit_to)]
            return labels
    
        def series(self):
            # Мы берем даты из `labels`, а данные из базы, где они могут 
            # быть не полными, например, в какой-нибудь день заказов
            # не окажется и это сломает график
            series = []
            for restaurant in self.legend:
                # Нам нужно убедиться, что если нет значений
                # за нужную дату, там будет стоять 0
                item = self.values.get(restaurant, {})
                series.append([item.get(label, 0) for label in self.labels])
            return series
    
        def values(self):
            # Лимит помноженный на число ресторанов
            limit_to = self.limit_to * len(self.legend)
            queryset = self.get_queryset()
            # Вот так в джанге можно сделать `GROUP BY` по двум полям: 
            # названию ресторана и даты.
            # Order.created это datetime, а групировка нужня по дням,
            # использем функцию `DATE` (sqlite3) для конвертации.
            # К сожалению, ORM джанги так устроена, что сортировать 
            # мы должны по тому же полю
            queryset = (queryset.extra({'baked':
                                        'DATE(created)'})
                                .select_related('restaurant')
                                .values_list('restaurant__name', 'baked')
                                .order_by('-baked')
                                .annotate(ocount=Count('pk'))[:limit_to])
    
            # Ключ -- ресторан, значение -- словарь дата:число_заказов
            values = defaultdict(dict)
            for restaurant, date, count in queryset:
                # DATE в Sqlite3 возвращает стрингу YYYY-MM-DD
                # А в чарте мы хотим видеть DD-MM
                day_month = '{2}.{1}'.format(*date.split('-'))
                values[restaurant][day_month] = count
            return values

    Дешбоарды


    django-controlcenter поддерживает до 10 дешбоардов. Но мы создадим один в pizzeria.dashboards

    class SimpleDashboard(Dashboard):
        widgets = (
            menu_widgets,
            latest_orders_widget,
            RestaurantSingleBarChart,
            PizzaSingleBarChart,
            OrderLineChart,
        )

    Вот и все, открываем /admin/dashboard/0/.

    Совместимость


    Тесты проводились на python 2.7.9, 3.4.3, 3.5.0 и django 1.8, 1.9.

    Name                                               Stmts   Miss  Cover
    ----------------------------------------------------------------------
    controlcenter/__init__.py                              1      0   100%
    controlcenter/app_settings.py                         27      0   100%
    controlcenter/base.py                                 10      0   100%
    controlcenter/dashboards.py                           27      0   100%
    controlcenter/templatetags/__init__.py                 0      0   100%
    controlcenter/templatetags/controlcenter_tags.py     109      0   100%
    controlcenter/utils.py                                16      0   100%
    controlcenter/views.py                                39      0   100%
    controlcenter/widgets/__init__.py                      2      0   100%
    controlcenter/widgets/charts.py                       67      0   100%
    controlcenter/widgets/core.py                         93      0   100%
    ----------------------------------------------------------------------
    TOTAL                                                391      0   100%
    _______________________________ summary ______________________________
      py27-django18: commands succeeded
      py27-django19: commands succeeded
      py34-django18: commands succeeded
      py34-django19: commands succeeded
      py35-django18: commands succeeded
      py35-django19: commands succeeded

    Так же приложение замечательно дружит с django-grappelli.

    Документация


    Эту статью можно считать таковой, перевод на корявый английский я выполню в ближайшее время, как только разберусь со sphinx.

    P.S. Я впервые решил заняться OSP и, надо признаться, больше потратил времени на разбирательства с дистрибьюцией, чем на сам код, и тем не менее я не до конца уверен, что все сделал правильно, поэтому буду признателен за любой фидбек.

    P.P.S. Спасибо дизайнерам хабра за то, что заголовоки не отличить от текста, а инлайновый код никак не выделяется. Я постараюсь как можно быстрее написать доки, потому что статью читать невозможно.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      0
      Спасибо за статью! Как раз то, чего джанге катастрофически не хватает. После праздников обязательно проверю ваше творение
        +1
        Да на здоровье. Кстати, в процессе появилась одна идея:

        # my_settings.py
        from app_settings import AppSettings
        
        class MyApplication(AppSettings):
            FOO = 'foo'
            BAR = 'bar'
        
        # project settings.py
        MYAPPLICATION_FOO = 'new foo'
        
        # some module
        import my_settings
        
        print my_settings.FOO, my_settings.BAR
        'new_foo' 'bar'

        Я нашел готовое решение, но что-то оно больно сложное, и в каждом третьем проекте есть свой велосипед. Отпишитесь, если кто встречал чего попроще, если нет, вынесу в отдельный пакет.
      +5
      Кто все эти люди? "дешбоарад", "дешбоард", "дешбоурдов","дешбоарда","дешбоардов"
        +5
        Спасибо, поправил. Я давно не был на хабре, раньше было принято писать о грамматических ошибках в личку, если, конечно, аудитории все еще интереснее обсуждать техническую сторону публикации.
        0
        Крутая штука, реально крутая. Вопрос такой. Очень часто разрабы советуют подключать свою админку через API. Почему было решено допиливать джанговскую?
          0
          Это не админка, скорее расширение. Технически джанга использует Model как апи, а ModelAdmin как конфигурацию. Model хранит в себе представление данных и их поведение (тип, валидации). Для формирования интерфейса используются мета-данные, вроде, названия приложения и имени модели — все это есть в Model, поэтому и я ее использую, к тому же джангистам все это знакомо — я не хотел сильно велосипедить. Только с этим есть одна проблема: джанга не умеет брать одну модель из СУБД, другую из монги, третью из редис и т.д. Мои обертки рассчитаны на это.
          0
          Добавьте документацию в репозиторий, хотя бы в минимальном виде, и ваша аудитория будет шире.
            0
            В работе, выложу на днях.
            0
            Огромное спасибо! Буквально вчера появилась необходимость реализовать подобный функционал в одном из проектов и тут Ваша статья.
              0
              На здоровье, жду фидбека.
              0
              Замечально! Но, хотелось бы пожелать вам упорства и терпения. Ну или хотя бы упертых и терпеливых коллег. Я имею ввиду, что хочется, что бы хорошие вещи поддерживались их авторами так долго, как это возможно
                0
                Спасибо, на подходе масенкая либа из второго коммента. Естественно, как закончу с доками для этой.
                +3
                Библиотека отличная. И супер, просто супер статья с объяснением, примерами!
                  0
                  Спасибо, если пригодится, обязательно отпишитесь.
                  0
                  Отлично!
                  Могу только как активный пользователь django-suit (к которому я больше тяготею, чем к grappelli) могу лишь только предложить в будущих релизах сделать поддержку и его тоже (а как разработчик пакета вы наверняка скажете "присылай мержреквест" — и будете правы :).
                    0
                    Очень круто будет еще добавить возможность экспорта циферок, которые видны на экране и в графиках в excel или csv, для офлайн анализа или обработки.
                      0
                      Сейчас пишу документацию и до меня дошло, что я упустил одну важную деталь: во момент инициализации виджета, ему передается объект request, т.е. вы можете делать так:

                      class OrdersWidget(widgets.ItemList):
                          model = Order
                      
                          def get_queryset(self):
                              queryset = super(OrdersWidget, self).get_queryset()
                              if not self.request.user.is_superuser:
                                  # Если это менеджер, ограничить выдачу его заказами
                                  return queryset.filter(manager=self.request.user)
                              return queryset
                        0
                          0

                          Обновил версию до 0.2.0


                          • безлимитное количество панелей
                          • используется django-pkgconf для хранения настроек

                          Only users with full accounts can post comments. Log in, please.