Source code for trionyx.widgets
"""
trionyx.widgets
~~~~~~~~~~~~~~~
:copyright: 2019 by Maikel Martens
:license: GPLv3
"""
import json
from django.utils import timezone
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.contrib.contenttypes.models import ContentType
from trionyx.trionyx.models import AuditLogEntry
from trionyx.renderer import renderer
from trionyx.config import models_config
from trionyx.trionyx.forms import AuditlogWidgetForm, TotalSummaryWidgetForm
from trionyx.models import Sum, filter_queryset_with_user_filters
from django.utils.translation import ugettext_lazy as _
widgets = {}
class MetaClass(type):
"""MetaClass for widget"""
def __new__(cls, clsname, bases, attrs):
"""Auto register new widget cass"""
newclass = super().__new__(cls, clsname, bases, attrs)
if clsname != 'BaseWidget':
widgets[newclass.code] = newclass
return newclass
[docs]class BaseWidget(metaclass=MetaClass):
"""
Base widget to extend for creating custom widgets.
Custom widgets are created in `widgets.py` in root of app folder.
**Example of random widget:**
.. code-block:: python
# <app dir>/widgets.py
RandomWidget(BaseWidget):
code = 'random'
name = 'Random widget'
description = 'Shows random string'
def get_data(self, request, config):
return utils.random_string(16)
.. code-block:: html
<!-- template path: widgets/random.html -->
<script type="text/x-template" id="widget-random-template">
<div :class="widgetClass">
<div class="box-header with-border">
<!-- Get title from config, your form fields are also available in the config -->
<h3 class="box-title">[[widget.config.title]]</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<!-- vue data property will be filled with the get_data results method --->
[[data]]
</div>
</div>
</script>
<script>
<!-- The component must be called `widget-<code>` -->
Vue.component('widget-random', {
mixins: [TxWidgetMixin],
template: '#widget-random-template',
});
</script>
"""
code = None
"""Code for widget"""
name = None
"""Name for widget is also used as default title"""
description = None
"""Short description on what the widget does"""
config_form_class = None
"""Form class used to change the widget. The form cleaned_data is used as the config"""
default_width = 4
"""Default width of widget, is based on grid system with max 12 columns"""
default_height = 20
"""Default height of widget, each step is 10px"""
@property
def template(self):
"""Template path `widgets/{code}.html` overwrite to set custom path"""
return 'widgets/{code}.html'.format(code=self.code)
@property
def image(self):
"""Image path `img/widgets/{code}.jpg` overwrite to set custom path"""
return 'img/widgets/{code}.jpg'.format(code=self.code)
[docs] def get_data(self, request, config):
"""Get data for widget, function needs te be overwritten on widget implementation"""
return None
@property
def config_fields(self):
"""Get the config field names"""
if not self.config_form_class:
return []
fields = list(self.config_form_class().base_fields)
for field in list(self.config_form_class().declared_fields):
if field not in fields:
fields.append(field)
return fields
class AuditlogWidget(BaseWidget):
"""Auditlog widget"""
code = 'auditlog'
name = _('Latest actions')
description = _('Show the latest tracked actions done by users and the system')
config_form_class = AuditlogWidgetForm
default_height = 22
def get_data(self, request, config):
"""Get data for widget"""
content_type_ids = [
content_type.id for model, content_type
in ContentType.objects.get_for_models(*[
config.model for config in models_config.get_all_configs(False)
if request.user.has_perm('{app_label}.view_{model_name}'.format(
app_label=config.app_label,
model_name=config.model_name,
).lower())
]).items()]
logs = AuditLogEntry.objects.filter(content_type__in=content_type_ids).prefetch_related('user')
show = config.get('show', 'all')
if show != 'all':
logs = logs.filter(user__isnull=show == 'system')
return [
{
'user_full_name': log.user.get_full_name() if log.user else _('System'),
'user_avatar': log.user.avatar.url if log.user and log.user.avatar else static('img/avatar.png'),
'action': renderer.render_field(log, 'action'),
'object': '({}) {}'.format(
log.content_type.model_class()._meta.verbose_name.capitalize(),
log.object_verbose_name),
'object_url': log.content_object.get_absolute_url() if log.content_object else '',
'created_at': renderer.render_field(log, 'created_at'),
} for log in logs.order_by('-created_at')[:6]
]
class TotalSummaryWidget(BaseWidget):
"""Total summary widget"""
code = 'total_summary'
name = _('Total summary')
description = _('Show total for given field on given period')
config_form_class = TotalSummaryWidgetForm
default_height = 5
def get_data(self, request, config):
"""Get data"""
ModelClass = ContentType.objects.get_for_id(config['model']).model_class()
query = ModelClass.objects.get_queryset()
if config.get('filters'):
query = filter_queryset_with_user_filters(query, json.loads(config['filters']))
if config.get('period', 'all') != 'all':
today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
query = query.filter(**{
'year': {
'{}__gte'.format(config['period_field']): today.replace(month=1, day=1)
},
'month': {
'{}__gte'.format(config['period_field']): today.replace(day=1)
},
'week': {
'{}__gte'.format(config['period_field']): today - timezone.timedelta(today.weekday())
},
'day': {
'{}__gte'.format(config['period_field']): today
},
'365days': {
'{}__gte'.format(config['period_field']): today - timezone.timedelta(days=365)
},
'30days': {
'{}__gte'.format(config['period_field']): today - timezone.timedelta(days=30)
},
'7days': {
'{}__gte'.format(config['period_field']): today - timezone.timedelta(days=7)
},
}.get(config['period'], {}))
if config.get('field', '__count__') == '__count__':
return query.count()
else:
result = query.aggregate(sum=Sum(config['field']))
return renderer.render_field(ModelClass(**{config['field']: result['sum']}), config['field'])