from django import forms from django.apps import apps from django.core.exceptions import PermissionDenied from django.urls.base import reverse, NoReverseMatch from django.template.context_processors import csrf from django.db.models.base import ModelBase from django.forms.forms import DeclarativeFieldsMetaclass from django.forms.utils import flatatt from django.template import loader from django.http import Http404 from django.test.client import RequestFactory from django.utils.encoding import force_text, smart_text from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.utils.http import urlencode, urlquote from django.views.decorators.cache import never_cache from xadmin import widgets as exwidgets from xadmin.layout import FormHelper from xadmin.models import UserSettings, UserWidget from xadmin.plugins.utils import get_context_dict from xadmin.sites import site from xadmin.views.base import CommAdminView, ModelAdminView, filter_hook, csrf_protect_m from xadmin.views.edit import CreateAdminView from xadmin.views.list import ListAdminView from xadmin.util import unquote import copy class WidgetTypeSelect(forms.Widget): def __init__(self, widgets, attrs=None): super(WidgetTypeSelect, self).__init__(attrs) self._widgets = widgets def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, extra_attrs={'name': name}) final_attrs['class'] = 'nav nav-pills nav-stacked' output = [u'' % flatatt(final_attrs)] options = self.render_options(force_text(value), final_attrs['id']) if options: output.append(options) output.append(u'') output.append('' % (final_attrs['id'], name, force_text(value))) return mark_safe(u'\n'.join(output)) def render_option(self, selected_choice, widget, id): if widget.widget_type == selected_choice: selected_html = u' class="active"' else: selected_html = '' return (u'

%s

%s

') % ( selected_html, widget.widget_icon, widget.widget_title or widget.widget_type, widget.description) def render_options(self, selected_choice, id): # Normalize to strings. output = [] for widget in self._widgets: output.append(self.render_option(selected_choice, widget, id)) return u'\n'.join(output) class UserWidgetAdmin(object): model_icon = 'fa fa-dashboard' list_display = ('widget_type', 'page_id', 'user') list_filter = ['user', 'widget_type', 'page_id'] list_display_links = ('widget_type',) user_fields = ['user'] hidden_menu = True wizard_form_list = ( (_(u"Widget Type"), ('page_id', 'widget_type')), (_(u"Widget Params"), {'callback': "get_widget_params_form", 'convert': "convert_widget_params"}) ) def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == 'widget_type': widgets = widget_manager.get_widgets(self.request.GET.get('page_id', '')) form_widget = WidgetTypeSelect(widgets) return forms.ChoiceField(choices=[(w.widget_type, w.description) for w in widgets], widget=form_widget, label=_('Widget Type')) if 'page_id' in self.request.GET and db_field.name == 'page_id': kwargs['widget'] = forms.HiddenInput field = super( UserWidgetAdmin, self).formfield_for_dbfield(db_field, **kwargs) return field def get_widget_params_form(self, wizard): data = wizard.get_cleaned_data_for_step(wizard.steps.first) widget_type = data['widget_type'] widget = widget_manager.get(widget_type) fields = copy.deepcopy(widget.base_fields) if 'id' in fields: del fields['id'] return DeclarativeFieldsMetaclass("WidgetParamsForm", (forms.Form,), fields) def convert_widget_params(self, wizard, cleaned_data, form): widget = UserWidget() value = dict([(f.name, f.value()) for f in form]) widget.set_value(value) cleaned_data['value'] = widget.value cleaned_data['user'] = self.user def get_list_display(self): list_display = super(UserWidgetAdmin, self).get_list_display() if not self.user.is_superuser: list_display.remove('user') return list_display def queryset(self): if self.user.is_superuser: return super(UserWidgetAdmin, self).queryset() return UserWidget.objects.filter(user=self.user) def update_dashboard(self, obj): try: portal_pos = UserSettings.objects.get( user=obj.user, key="dashboard:%s:pos" % obj.page_id) except UserSettings.DoesNotExist: return pos = [[w for w in col.split(',') if w != str( obj.id)] for col in portal_pos.value.split('|')] portal_pos.value = '|'.join([','.join(col) for col in pos]) portal_pos.save() def delete_model(self): self.update_dashboard(self.obj) super(UserWidgetAdmin, self).delete_model() def delete_models(self, queryset): for obj in queryset: self.update_dashboard(obj) super(UserWidgetAdmin, self).delete_models(queryset) site.register(UserWidget, UserWidgetAdmin) class WidgetManager(object): _widgets = None def __init__(self): self._widgets = {} def register(self, widget_class): self._widgets[widget_class.widget_type] = widget_class return widget_class def get(self, name): return self._widgets[name] def get_widgets(self, page_id): return self._widgets.values() widget_manager = WidgetManager() class WidgetDataError(Exception): def __init__(self, widget, errors): super(WidgetDataError, self).__init__(str(errors)) self.widget = widget self.errors = errors class BaseWidget(forms.Form): template = 'xadmin/widgets/base.html' description = 'Base Widget, don\'t use it.' widget_title = None widget_icon = 'fa fa-plus-square' widget_type = 'base' base_title = None id = forms.IntegerField(label=_('Widget ID'), widget=forms.HiddenInput) title = forms.CharField(label=_('Widget Title'), required=False, widget=exwidgets.AdminTextInputWidget) def __init__(self, dashboard, data): self.dashboard = dashboard self.admin_site = dashboard.admin_site self.request = dashboard.request self.user = dashboard.request.user self.convert(data) super(BaseWidget, self).__init__(data) if not self.is_valid(): raise WidgetDataError(self, self.errors.as_text()) self.setup() def setup(self): helper = FormHelper() helper.form_tag = False helper.include_media = False self.helper = helper self.id = self.cleaned_data['id'] self.title = self.cleaned_data['title'] or self.base_title if not (self.user.is_superuser or self.has_perm()): raise PermissionDenied @property def widget(self): context = {'widget_id': self.id, 'widget_title': self.title, 'widget_icon': self.widget_icon, 'widget_type': self.widget_type, 'form': self, 'widget': self} context.update(csrf(self.request)) self.context(context) return loader.render_to_string(self.template, context) def context(self, context): pass def convert(self, data): pass def has_perm(self): return False def save(self): value = dict([(f.name, f.value()) for f in self]) user_widget = UserWidget.objects.get(id=self.id) user_widget.set_value(value) user_widget.save() def static(self, path): return self.dashboard.static(path) def vendor(self, *tags): return self.dashboard.vendor(*tags) def media(self): return forms.Media() @widget_manager.register class HtmlWidget(BaseWidget): widget_type = 'html' widget_icon = 'fa fa-file-o' description = _( u'Html Content Widget, can write any html content in widget.') content = forms.CharField(label=_( 'Html Content'), widget=exwidgets.AdminTextareaWidget, required=False) def has_perm(self): return True def context(self, context): context['content'] = self.cleaned_data['content'] class ModelChoiceIterator(object): def __init__(self, field): self.field = field def __iter__(self): from xadmin import site as g_admin_site for m, ma in g_admin_site._registry.items(): yield ('%s.%s' % (m._meta.app_label, m._meta.model_name), m._meta.verbose_name) class ModelChoiceField(forms.ChoiceField): def __init__(self, *, required=True, widget=None, label=None, initial=None, help_text=None, **kwargs): # Call Field instead of ChoiceField __init__() because we don't need # ChoiceField.__init__(). forms.Field.__init__(self, **kwargs) self.widget.choices = self.choices def __deepcopy__(self, memo): result = forms.Field.__deepcopy__(self, memo) return result def _get_choices(self): return ModelChoiceIterator(self) choices = property(_get_choices, forms.ChoiceField._set_choices) def to_python(self, value): if isinstance(value, ModelBase): return value app_label, model_name = value.lower().split('.') return apps.get_model(app_label, model_name) def prepare_value(self, value): if isinstance(value, ModelBase): value = '%s.%s' % (value._meta.app_label, value._meta.model_name) return value def valid_value(self, value): value = self.prepare_value(value) for k, v in self.choices: if value == smart_text(k): return True return False class ModelBaseWidget(BaseWidget): app_label = None model_name = None model_perm = 'change' model = ModelChoiceField(label=_(u'Target Model'), widget=exwidgets.AdminSelectWidget) def __init__(self, dashboard, data): self.dashboard = dashboard super(ModelBaseWidget, self).__init__(dashboard, data) def setup(self): self.model = self.cleaned_data['model'] self.app_label = self.model._meta.app_label self.model_name = self.model._meta.model_name super(ModelBaseWidget, self).setup() def has_perm(self): return self.dashboard.has_model_perm(self.model, self.model_perm) def filte_choices_model(self, model, modeladmin): return self.dashboard.has_model_perm(model, self.model_perm) def model_admin_url(self, name, *args, **kwargs): return reverse( "%s:%s_%s_%s" % (self.admin_site.app_name, self.app_label, self.model_name, name), args=args, kwargs=kwargs) class PartialBaseWidget(BaseWidget): def get_view_class(self, view_class, model=None, **opts): admin_class = self.admin_site._registry.get(model) if model else None return self.admin_site.get_view_class(view_class, admin_class, **opts) def get_factory(self): return RequestFactory() def setup_request(self, request): request.user = self.user request.session = self.request.session return request def make_get_request(self, path, data={}, **extra): req = self.get_factory().get(path, data, **extra) return self.setup_request(req) def make_post_request(self, path, data={}, **extra): req = self.get_factory().post(path, data, **extra) return self.setup_request(req) @widget_manager.register class QuickBtnWidget(BaseWidget): widget_type = 'qbutton' description = _(u'Quick button Widget, quickly open any page.') template = "xadmin/widgets/qbutton.html" base_title = _(u"Quick Buttons") widget_icon = 'fa fa-caret-square-o-right' def convert(self, data): self.q_btns = data.pop('btns', []) def get_model(self, model_or_label): if isinstance(model_or_label, ModelBase): return model_or_label else: return apps.get_model(*model_or_label.lower().split('.')) def context(self, context): btns = [] for b in self.q_btns: btn = {} if 'model' in b: model = self.get_model(b['model']) if not self.user.has_perm("%s.view_%s" % (model._meta.app_label, model._meta.model_name)): continue btn['url'] = reverse("%s:%s_%s_%s" % (self.admin_site.app_name, model._meta.app_label, model._meta.model_name, b.get('view', 'changelist'))) btn['title'] = model._meta.verbose_name btn['icon'] = self.dashboard.get_model_icon(model) else: try: btn['url'] = reverse(b['url']) except NoReverseMatch: btn['url'] = b['url'] if 'title' in b: btn['title'] = b['title'] if 'icon' in b: btn['icon'] = b['icon'] btns.append(btn) context.update({'btns': btns}) def has_perm(self): return True @widget_manager.register class ListWidget(ModelBaseWidget, PartialBaseWidget): widget_type = 'list' description = _(u'Any Objects list Widget.') template = "xadmin/widgets/list.html" model_perm = 'view' widget_icon = 'fa fa-align-justify' def convert(self, data): self.list_params = data.pop('params', {}) self.list_count = data.pop('count', 10) def setup(self): super(ListWidget, self).setup() if not self.title: self.title = self.model._meta.verbose_name_plural req = self.make_get_request("", self.list_params) self.list_view = self.get_view_class(ListAdminView, self.model)(req) if self.list_count: self.list_view.list_per_page = self.list_count def context(self, context): list_view = self.list_view list_view.make_result_list() base_fields = list_view.base_list_display if len(base_fields) > 5: base_fields = base_fields[0:5] context['result_headers'] = [c for c in list_view.result_headers( ).cells if c.field_name in base_fields] context['results'] = [[o for i, o in enumerate(filter(lambda c:c.field_name in base_fields, r.cells))] for r in list_view.results()] context['result_count'] = list_view.result_count context['page_url'] = self.model_admin_url('changelist') + "?" + urlencode(self.list_params) @widget_manager.register class AddFormWidget(ModelBaseWidget, PartialBaseWidget): widget_type = 'addform' description = _(u'Add any model object Widget.') template = "xadmin/widgets/addform.html" model_perm = 'add' widget_icon = 'fa fa-plus' def setup(self): super(AddFormWidget, self).setup() if self.title is None: self.title = _('Add %s') % self.model._meta.verbose_name req = self.make_get_request("") self.add_view = self.get_view_class( CreateAdminView, self.model, list_per_page=10)(req) self.add_view.instance_forms() def context(self, context): helper = FormHelper() helper.form_tag = False helper.include_media = False context.update({ 'addform': self.add_view.form_obj, 'addhelper': helper, 'addurl': self.add_view.model_admin_url('add'), 'model': self.model }) def media(self): return self.add_view.media + self.add_view.form_obj.media + self.vendor('xadmin.plugin.quick-form.js') class Dashboard(CommAdminView): widget_customiz = True widgets = [] title = _(u"Dashboard") icon = None def get_page_id(self): return self.request.path def get_portal_key(self): return "dashboard:%s:pos" % self.get_page_id() @filter_hook def get_widget(self, widget_or_id, data=None): try: if isinstance(widget_or_id, UserWidget): widget = widget_or_id else: widget = UserWidget.objects.get(user=self.user, page_id=self.get_page_id(), id=widget_or_id) wid = widget_manager.get(widget.widget_type) class widget_with_perm(wid): def context(self, context): super(widget_with_perm, self).context(context) context.update({'has_change_permission': self.request.user.has_perm('xadmin.change_userwidget')}) wid_instance = widget_with_perm(self, data or widget.get_value()) return wid_instance except UserWidget.DoesNotExist: return None @filter_hook def get_init_widget(self): portal = [] widgets = self.widgets for col in widgets: portal_col = [] for opts in col: try: widget = UserWidget(user=self.user, page_id=self.get_page_id(), widget_type=opts['type']) widget.set_value(opts) widget.save() portal_col.append(self.get_widget(widget)) except (PermissionDenied, WidgetDataError): widget.delete() continue portal.append(portal_col) UserSettings( user=self.user, key="dashboard:%s:pos" % self.get_page_id(), value='|'.join([','.join([str(w.id) for w in col]) for col in portal])).save() return portal @filter_hook def get_widgets(self): if self.widget_customiz: portal_pos = UserSettings.objects.filter( user=self.user, key=self.get_portal_key()) if len(portal_pos): portal_pos = portal_pos[0].value widgets = [] if portal_pos: user_widgets = dict([(uw.id, uw) for uw in UserWidget.objects.filter(user=self.user, page_id=self.get_page_id())]) for col in portal_pos.split('|'): ws = [] for wid in col.split(','): try: widget = user_widgets.get(int(wid)) if widget: ws.append(self.get_widget(widget)) except Exception as e: import logging logging.error(e, exc_info=True) widgets.append(ws) return widgets return self.get_init_widget() @filter_hook def get_title(self): return self.title @filter_hook def get_context(self): new_context = { 'title': self.get_title(), 'icon': self.icon, 'portal_key': self.get_portal_key(), 'columns': [('col-sm-%d' % int(12 / len(self.widgets)), ws) for ws in self.widgets], 'has_add_widget_permission': self.has_model_perm(UserWidget, 'add') and self.widget_customiz, 'add_widget_url': self.get_admin_url('%s_%s_add' % (UserWidget._meta.app_label, UserWidget._meta.model_name)) + "?user=%s&page_id=%s&_redirect=%s" % (self.user.id, self.get_page_id(), urlquote(self.request.get_full_path())) } context = super(Dashboard, self).get_context() context.update(new_context) return context @never_cache def get(self, request, *args, **kwargs): self.widgets = self.get_widgets() return self.template_response('xadmin/views/dashboard.html', self.get_context()) @csrf_protect_m def post(self, request, *args, **kwargs): if 'id' in request.POST: widget_id = request.POST['id'] if request.POST.get('_delete', None) != 'on': widget = self.get_widget(widget_id, request.POST.copy()) widget.save() else: try: widget = UserWidget.objects.get( user=self.user, page_id=self.get_page_id(), id=widget_id) widget.delete() try: portal_pos = UserSettings.objects.get(user=self.user, key="dashboard:%s:pos" % self.get_page_id()) pos = [[w for w in col.split(',') if w != str( widget_id)] for col in portal_pos.value.split('|')] portal_pos.value = '|'.join([','.join(col) for col in pos]) portal_pos.save() except Exception: pass except UserWidget.DoesNotExist: pass return self.get(request) @filter_hook def get_media(self): media = super(Dashboard, self).get_media() + \ self.vendor('xadmin.page.dashboard.js', 'xadmin.page.dashboard.css') if self.widget_customiz: media = media + self.vendor('xadmin.plugin.portal.js') for ws in self.widgets: for widget in ws: media = media + widget.media() return media class ModelDashboard(Dashboard, ModelAdminView): title = _(u"%s Dashboard") def get_page_id(self): return 'model:%s/%s' % self.model_info @filter_hook def get_title(self): return self.title % force_text(self.obj) def init_request(self, object_id, *args, **kwargs): self.obj = self.get_object(unquote(object_id)) if not self.has_view_permission(self.obj): raise PermissionDenied if self.obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(self.opts.verbose_name), 'key': escape(object_id)}) @filter_hook def get_context(self): new_context = { 'has_change_permission': self.has_change_permission(self.obj), 'object': self.obj, } context = Dashboard.get_context(self) context.update(ModelAdminView.get_context(self)) context.update(new_context) return context @never_cache def get(self, request, *args, **kwargs): self.widgets = self.get_widgets() return self.template_response(self.get_template_list('views/model_dashboard.html'), self.get_context())