
# 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' ] ItemList limited in output so as not to tear the page down to you).
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 and Chart . There is also a Group , but this is not a widget, but a wrapper. Let's start with it.Group . class MyDashboard(Dashboard): widgets = ( Foo, (Bar, Baz), Group((Egg, Spam), width=widgets.LARGE, height=300, attrs={'class': 'my_class', 'data-foo': 'foo'}), ) Group takes three optional arguments: width , height , attrs .768px widgets occupy the entire width, then 50% or 100% . From 1000px used 6-column grid. For convenience, the values are stored in the widgets module: # controlcenter/widgets.py MEDIUM = 2 # 33% [x] + [x] + [x] LARGE = 3 # 50% [ x ] + [ x ] LARGER = 4 # 66% [ x ] + [x] LARGEST = 6 # 100% [ x ] None , but having received an integer, set this value to the widget as max-height and an optional scroll appears.width and height , in case these values are not specified in the Group , the maximum value is taken from the widgets in this group.html attribute. You can even set the id .values method (and series , labels , legend for charts) in the cached_property descriptor. Accordingly, values are available as when accessing an attribute (without calling), and data is cached. This is just a small convenience, as you often need to refer to these methods. For example, the following is done for charts: 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') Group (see above).queryset , it will return.model , it will return its default manager.get_queryset .get_queryset . At least from the file read. Also limits the queriset to limit_to if it is not None , like this: self.get_queryset()[:self.limit_to] .Widget.template_name_prefix + Widget.template_name . class ModelItemList(widgets.ItemList): model = Model queryset = model.objects.all() list_display = ['pk', 'field'] values are taken from the keys of list_display (for models, dictionaries and namedtuple), for sequences the key index is equal to the value index, roughly speaking zip(list_display, values) .# to list_display and get line numbering. Also, the "grid" can be replaced with another symbol by setting it as a value in settings.CONTROLCENTER_SHARP .ItemList tries to hang a link to the object editing page in the admin panel, for this it needs an object class and a primary key. Therefore, the widget will search for this data everywhere: if values return the model instance, it will pull everything out of it. If values returns a dictionary, a list or a namedtuple , then you will need to specify ItemList.model , because, of course, no longer where. In all cases, the widget will try to find pk or id its own, but in the case of sequences, this will not work, so the widget will look for these keys in list_display matching its index with the index of sequence values.deferred models, so you can write like this: queryset = Model.obejcts.defer('field') .ModelAdmin builds such paths on its own. But in the widget you can substitute anything in the queryset , so you have to help. There are several options: 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' sortable=True , but remember that janga sorts in the database, and the widget on the client side, so incidents can occur, for example, if the dates in the column are in the dd.mm format. The sortable.js library is used .values returns an empty list.10 , so you don't shoot yourself in the leg.LINE , BAR , PIE ; and their respective classes: LineChart , BarChart , PieChart . Plus a few extras, more on that later.Chart defines three additional methods: legend , lables , series , which are also cached. All three methods must return a json-serializable object that does not include generators. class MyChart(widgets.Chart): def legend(self): return [] def labels(self): return [] def series(self): return [] x axis. Should return a sequence, in no case do not pass the generator.y axis. Must return a list of lists, since there may be multiple data in the graphs. Again, no generators. There is a small “Gotcha” here, for the type BAR with one type of value, the “flat” list is transferred, i.e. not nested, this sets an additional option for the Chartist. The easiest way to use SingleBarChart - everything is set up in it.Chart is a widget with an additional Chartist class inside in Meta or Media style in jang. class MyChart(Chart): class Chartist: klass = widgets.LINE point_lables = True options = { 'reverseData': True, 'axisY': { 'onlyInteger': True, }, 'fullWidth': True, } Chartist not necessary to inherit the parent class, i.e. this is not the classic python inheritance: you write class Chartist: not class Chartist(Parent.Chartist): - fields are inherited automatically. In the inheriting class, all fields are rewritten, except options , which is glued to the parent, i.e. in the child class, you can write only new key / value pairs, not Parent.Chartist.options.copy().update({'foo': 'bar'}) . Of course, this method has a downside: the default values, if necessary, will have to be rewritten.LineChart set to 'reverseData': True , which reverses the values of labels and series on the client. Most often, this type of charts is used to display the latest data, and so that you do not have to do it manually in every first chart, this option is enabled by default.widgets.LINE , widgets.BAR , widgets.PIE .Chartist , which Chartist the values on the chart. This is strange, but the default Chartist does without values on the chart itself. Unfortunately, this thing only works with widgets.LINE . In other cases, the legend method will help.widgets module, several more auxiliary classes are SingleLineChart : SingleLineChart , SingleBarChart , SinglePieChart - for simple use cases. class BlogsChart(widgets.SingleBarChart): model = Blog values_list = ('name', 'score') name values will go to the x axis, and the score to the y axis./admin/dashboards/[pk]/ - where pk index in the list of settings.CONTROLCENTER_DASHBOARDS . # CONTROLCENTER_DASHBOARDS = [] # `ItemList` CONTROLCENTER_SHARP = '#' # . `Chartist`, # `Material Design`, # `material`. CONTROLCENTER_CHARTIST_COLORS = 'default' pizzeria , add the pizza application to it. 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 applications to pizzeria.settings INSTALLED_APPS = ( ... 'controlcenter', 'pizza', ) # CONTROLCENTER_DASHBOARDS = ( 'pizzeria.dashboards.MyDashboard' ) pizzeria.urls to 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 file pizzeria.dashboards create widgets: 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 pizzeria.dashboards class SimpleDashboard(Dashboard): widgets = ( menu_widgets, latest_orders_widget, RestaurantSingleBarChart, PizzaSingleBarChart, OrderLineChart, ) /admin/dashboard/0/ . 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 Source: https://habr.com/ru/post/278743/
All Articles