dashboard.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. from django import forms
  2. from django.apps import apps
  3. from django.core.exceptions import PermissionDenied
  4. from django.urls.base import reverse, NoReverseMatch
  5. from django.template.context_processors import csrf
  6. from django.db.models.base import ModelBase
  7. from django.forms.forms import DeclarativeFieldsMetaclass
  8. from django.forms.utils import flatatt
  9. from django.template import loader
  10. from django.http import Http404
  11. from django.test.client import RequestFactory
  12. from django.utils.encoding import force_text, smart_text
  13. from django.utils.html import escape
  14. from django.utils.safestring import mark_safe
  15. from django.utils.translation import ugettext as _
  16. from django.utils.http import urlencode, urlquote
  17. from django.views.decorators.cache import never_cache
  18. from xadmin import widgets as exwidgets
  19. from xadmin.layout import FormHelper
  20. from xadmin.models import UserSettings, UserWidget
  21. from xadmin.plugins.utils import get_context_dict
  22. from xadmin.sites import site
  23. from xadmin.views.base import CommAdminView, ModelAdminView, filter_hook, csrf_protect_m
  24. from xadmin.views.edit import CreateAdminView
  25. from xadmin.views.list import ListAdminView
  26. from xadmin.util import unquote
  27. import copy
  28. class WidgetTypeSelect(forms.Widget):
  29. def __init__(self, widgets, attrs=None):
  30. super(WidgetTypeSelect, self).__init__(attrs)
  31. self._widgets = widgets
  32. def render(self, name, value, attrs=None):
  33. if value is None:
  34. value = ''
  35. final_attrs = self.build_attrs(attrs, extra_attrs={'name': name})
  36. final_attrs['class'] = 'nav nav-pills nav-stacked'
  37. output = [u'<ul%s>' % flatatt(final_attrs)]
  38. options = self.render_options(force_text(value), final_attrs['id'])
  39. if options:
  40. output.append(options)
  41. output.append(u'</ul>')
  42. output.append('<input type="hidden" id="%s_input" name="%s" value="%s"/>' %
  43. (final_attrs['id'], name, force_text(value)))
  44. return mark_safe(u'\n'.join(output))
  45. def render_option(self, selected_choice, widget, id):
  46. if widget.widget_type == selected_choice:
  47. selected_html = u' class="active"'
  48. else:
  49. selected_html = ''
  50. return (u'<li%s><a onclick="' +
  51. 'javascript:$(this).parent().parent().find(\'>li\').removeClass(\'active\');$(this).parent().addClass(\'active\');' +
  52. '$(\'#%s_input\').attr(\'value\', \'%s\')' % (id, widget.widget_type) +
  53. '"><h4><i class="%s"></i> %s</h4><p>%s</p></a></li>') % (
  54. selected_html,
  55. widget.widget_icon,
  56. widget.widget_title or widget.widget_type,
  57. widget.description)
  58. def render_options(self, selected_choice, id):
  59. # Normalize to strings.
  60. output = []
  61. for widget in self._widgets:
  62. output.append(self.render_option(selected_choice, widget, id))
  63. return u'\n'.join(output)
  64. class UserWidgetAdmin(object):
  65. model_icon = 'fa fa-dashboard'
  66. list_display = ('widget_type', 'page_id', 'user')
  67. list_filter = ['user', 'widget_type', 'page_id']
  68. list_display_links = ('widget_type',)
  69. user_fields = ['user']
  70. hidden_menu = True
  71. wizard_form_list = (
  72. (_(u"Widget Type"), ('page_id', 'widget_type')),
  73. (_(u"Widget Params"), {'callback':
  74. "get_widget_params_form", 'convert': "convert_widget_params"})
  75. )
  76. def formfield_for_dbfield(self, db_field, **kwargs):
  77. if db_field.name == 'widget_type':
  78. widgets = widget_manager.get_widgets(self.request.GET.get('page_id', ''))
  79. form_widget = WidgetTypeSelect(widgets)
  80. return forms.ChoiceField(choices=[(w.widget_type, w.description) for w in widgets],
  81. widget=form_widget, label=_('Widget Type'))
  82. if 'page_id' in self.request.GET and db_field.name == 'page_id':
  83. kwargs['widget'] = forms.HiddenInput
  84. field = super(
  85. UserWidgetAdmin, self).formfield_for_dbfield(db_field, **kwargs)
  86. return field
  87. def get_widget_params_form(self, wizard):
  88. data = wizard.get_cleaned_data_for_step(wizard.steps.first)
  89. widget_type = data['widget_type']
  90. widget = widget_manager.get(widget_type)
  91. fields = copy.deepcopy(widget.base_fields)
  92. if 'id' in fields:
  93. del fields['id']
  94. return DeclarativeFieldsMetaclass("WidgetParamsForm", (forms.Form,), fields)
  95. def convert_widget_params(self, wizard, cleaned_data, form):
  96. widget = UserWidget()
  97. value = dict([(f.name, f.value()) for f in form])
  98. widget.set_value(value)
  99. cleaned_data['value'] = widget.value
  100. cleaned_data['user'] = self.user
  101. def get_list_display(self):
  102. list_display = super(UserWidgetAdmin, self).get_list_display()
  103. if not self.user.is_superuser:
  104. list_display.remove('user')
  105. return list_display
  106. def queryset(self):
  107. if self.user.is_superuser:
  108. return super(UserWidgetAdmin, self).queryset()
  109. return UserWidget.objects.filter(user=self.user)
  110. def update_dashboard(self, obj):
  111. try:
  112. portal_pos = UserSettings.objects.get(
  113. user=obj.user, key="dashboard:%s:pos" % obj.page_id)
  114. except UserSettings.DoesNotExist:
  115. return
  116. pos = [[w for w in col.split(',') if w != str(
  117. obj.id)] for col in portal_pos.value.split('|')]
  118. portal_pos.value = '|'.join([','.join(col) for col in pos])
  119. portal_pos.save()
  120. def delete_model(self):
  121. self.update_dashboard(self.obj)
  122. super(UserWidgetAdmin, self).delete_model()
  123. def delete_models(self, queryset):
  124. for obj in queryset:
  125. self.update_dashboard(obj)
  126. super(UserWidgetAdmin, self).delete_models(queryset)
  127. site.register(UserWidget, UserWidgetAdmin)
  128. class WidgetManager(object):
  129. _widgets = None
  130. def __init__(self):
  131. self._widgets = {}
  132. def register(self, widget_class):
  133. self._widgets[widget_class.widget_type] = widget_class
  134. return widget_class
  135. def get(self, name):
  136. return self._widgets[name]
  137. def get_widgets(self, page_id):
  138. return self._widgets.values()
  139. widget_manager = WidgetManager()
  140. class WidgetDataError(Exception):
  141. def __init__(self, widget, errors):
  142. super(WidgetDataError, self).__init__(str(errors))
  143. self.widget = widget
  144. self.errors = errors
  145. class BaseWidget(forms.Form):
  146. template = 'xadmin/widgets/base.html'
  147. description = 'Base Widget, don\'t use it.'
  148. widget_title = None
  149. widget_icon = 'fa fa-plus-square'
  150. widget_type = 'base'
  151. base_title = None
  152. id = forms.IntegerField(label=_('Widget ID'), widget=forms.HiddenInput)
  153. title = forms.CharField(label=_('Widget Title'), required=False, widget=exwidgets.AdminTextInputWidget)
  154. def __init__(self, dashboard, data):
  155. self.dashboard = dashboard
  156. self.admin_site = dashboard.admin_site
  157. self.request = dashboard.request
  158. self.user = dashboard.request.user
  159. self.convert(data)
  160. super(BaseWidget, self).__init__(data)
  161. if not self.is_valid():
  162. raise WidgetDataError(self, self.errors.as_text())
  163. self.setup()
  164. def setup(self):
  165. helper = FormHelper()
  166. helper.form_tag = False
  167. helper.include_media = False
  168. self.helper = helper
  169. self.id = self.cleaned_data['id']
  170. self.title = self.cleaned_data['title'] or self.base_title
  171. if not (self.user.is_superuser or self.has_perm()):
  172. raise PermissionDenied
  173. @property
  174. def widget(self):
  175. context = {'widget_id': self.id, 'widget_title': self.title, 'widget_icon': self.widget_icon,
  176. 'widget_type': self.widget_type, 'form': self, 'widget': self}
  177. context.update(csrf(self.request))
  178. self.context(context)
  179. return loader.render_to_string(self.template, context)
  180. def context(self, context):
  181. pass
  182. def convert(self, data):
  183. pass
  184. def has_perm(self):
  185. return False
  186. def save(self):
  187. value = dict([(f.name, f.value()) for f in self])
  188. user_widget = UserWidget.objects.get(id=self.id)
  189. user_widget.set_value(value)
  190. user_widget.save()
  191. def static(self, path):
  192. return self.dashboard.static(path)
  193. def vendor(self, *tags):
  194. return self.dashboard.vendor(*tags)
  195. def media(self):
  196. return forms.Media()
  197. @widget_manager.register
  198. class HtmlWidget(BaseWidget):
  199. widget_type = 'html'
  200. widget_icon = 'fa fa-file-o'
  201. description = _(
  202. u'Html Content Widget, can write any html content in widget.')
  203. content = forms.CharField(label=_(
  204. 'Html Content'), widget=exwidgets.AdminTextareaWidget, required=False)
  205. def has_perm(self):
  206. return True
  207. def context(self, context):
  208. context['content'] = self.cleaned_data['content']
  209. class ModelChoiceIterator(object):
  210. def __init__(self, field):
  211. self.field = field
  212. def __iter__(self):
  213. from xadmin import site as g_admin_site
  214. for m, ma in g_admin_site._registry.items():
  215. yield ('%s.%s' % (m._meta.app_label, m._meta.model_name),
  216. m._meta.verbose_name)
  217. class ModelChoiceField(forms.ChoiceField):
  218. def __init__(self, *, required=True, widget=None, label=None, initial=None,
  219. help_text=None, **kwargs):
  220. # Call Field instead of ChoiceField __init__() because we don't need
  221. # ChoiceField.__init__().
  222. forms.Field.__init__(self, **kwargs)
  223. self.widget.choices = self.choices
  224. def __deepcopy__(self, memo):
  225. result = forms.Field.__deepcopy__(self, memo)
  226. return result
  227. def _get_choices(self):
  228. return ModelChoiceIterator(self)
  229. choices = property(_get_choices, forms.ChoiceField._set_choices)
  230. def to_python(self, value):
  231. if isinstance(value, ModelBase):
  232. return value
  233. app_label, model_name = value.lower().split('.')
  234. return apps.get_model(app_label, model_name)
  235. def prepare_value(self, value):
  236. if isinstance(value, ModelBase):
  237. value = '%s.%s' % (value._meta.app_label, value._meta.model_name)
  238. return value
  239. def valid_value(self, value):
  240. value = self.prepare_value(value)
  241. for k, v in self.choices:
  242. if value == smart_text(k):
  243. return True
  244. return False
  245. class ModelBaseWidget(BaseWidget):
  246. app_label = None
  247. model_name = None
  248. model_perm = 'change'
  249. model = ModelChoiceField(label=_(u'Target Model'), widget=exwidgets.AdminSelectWidget)
  250. def __init__(self, dashboard, data):
  251. self.dashboard = dashboard
  252. super(ModelBaseWidget, self).__init__(dashboard, data)
  253. def setup(self):
  254. self.model = self.cleaned_data['model']
  255. self.app_label = self.model._meta.app_label
  256. self.model_name = self.model._meta.model_name
  257. super(ModelBaseWidget, self).setup()
  258. def has_perm(self):
  259. return self.dashboard.has_model_perm(self.model, self.model_perm)
  260. def filte_choices_model(self, model, modeladmin):
  261. return self.dashboard.has_model_perm(model, self.model_perm)
  262. def model_admin_url(self, name, *args, **kwargs):
  263. return reverse(
  264. "%s:%s_%s_%s" % (self.admin_site.app_name, self.app_label,
  265. self.model_name, name), args=args, kwargs=kwargs)
  266. class PartialBaseWidget(BaseWidget):
  267. def get_view_class(self, view_class, model=None, **opts):
  268. admin_class = self.admin_site._registry.get(model) if model else None
  269. return self.admin_site.get_view_class(view_class, admin_class, **opts)
  270. def get_factory(self):
  271. return RequestFactory()
  272. def setup_request(self, request):
  273. request.user = self.user
  274. request.session = self.request.session
  275. return request
  276. def make_get_request(self, path, data={}, **extra):
  277. req = self.get_factory().get(path, data, **extra)
  278. return self.setup_request(req)
  279. def make_post_request(self, path, data={}, **extra):
  280. req = self.get_factory().post(path, data, **extra)
  281. return self.setup_request(req)
  282. @widget_manager.register
  283. class QuickBtnWidget(BaseWidget):
  284. widget_type = 'qbutton'
  285. description = _(u'Quick button Widget, quickly open any page.')
  286. template = "xadmin/widgets/qbutton.html"
  287. base_title = _(u"Quick Buttons")
  288. widget_icon = 'fa fa-caret-square-o-right'
  289. def convert(self, data):
  290. self.q_btns = data.pop('btns', [])
  291. def get_model(self, model_or_label):
  292. if isinstance(model_or_label, ModelBase):
  293. return model_or_label
  294. else:
  295. return apps.get_model(*model_or_label.lower().split('.'))
  296. def context(self, context):
  297. btns = []
  298. for b in self.q_btns:
  299. btn = {}
  300. if 'model' in b:
  301. model = self.get_model(b['model'])
  302. if not self.user.has_perm("%s.view_%s" % (model._meta.app_label, model._meta.model_name)):
  303. continue
  304. btn['url'] = reverse("%s:%s_%s_%s" % (self.admin_site.app_name, model._meta.app_label,
  305. model._meta.model_name, b.get('view', 'changelist')))
  306. btn['title'] = model._meta.verbose_name
  307. btn['icon'] = self.dashboard.get_model_icon(model)
  308. else:
  309. try:
  310. btn['url'] = reverse(b['url'])
  311. except NoReverseMatch:
  312. btn['url'] = b['url']
  313. if 'title' in b:
  314. btn['title'] = b['title']
  315. if 'icon' in b:
  316. btn['icon'] = b['icon']
  317. btns.append(btn)
  318. context.update({'btns': btns})
  319. def has_perm(self):
  320. return True
  321. @widget_manager.register
  322. class ListWidget(ModelBaseWidget, PartialBaseWidget):
  323. widget_type = 'list'
  324. description = _(u'Any Objects list Widget.')
  325. template = "xadmin/widgets/list.html"
  326. model_perm = 'view'
  327. widget_icon = 'fa fa-align-justify'
  328. def convert(self, data):
  329. self.list_params = data.pop('params', {})
  330. self.list_count = data.pop('count', 10)
  331. def setup(self):
  332. super(ListWidget, self).setup()
  333. if not self.title:
  334. self.title = self.model._meta.verbose_name_plural
  335. req = self.make_get_request("", self.list_params)
  336. self.list_view = self.get_view_class(ListAdminView, self.model)(req)
  337. if self.list_count:
  338. self.list_view.list_per_page = self.list_count
  339. def context(self, context):
  340. list_view = self.list_view
  341. list_view.make_result_list()
  342. base_fields = list_view.base_list_display
  343. if len(base_fields) > 5:
  344. base_fields = base_fields[0:5]
  345. context['result_headers'] = [c for c in list_view.result_headers(
  346. ).cells if c.field_name in base_fields]
  347. context['results'] = [[o for i, o in
  348. enumerate(filter(lambda c:c.field_name in base_fields, r.cells))]
  349. for r in list_view.results()]
  350. context['result_count'] = list_view.result_count
  351. context['page_url'] = self.model_admin_url('changelist') + "?" + urlencode(self.list_params)
  352. @widget_manager.register
  353. class AddFormWidget(ModelBaseWidget, PartialBaseWidget):
  354. widget_type = 'addform'
  355. description = _(u'Add any model object Widget.')
  356. template = "xadmin/widgets/addform.html"
  357. model_perm = 'add'
  358. widget_icon = 'fa fa-plus'
  359. def setup(self):
  360. super(AddFormWidget, self).setup()
  361. if self.title is None:
  362. self.title = _('Add %s') % self.model._meta.verbose_name
  363. req = self.make_get_request("")
  364. self.add_view = self.get_view_class(
  365. CreateAdminView, self.model, list_per_page=10)(req)
  366. self.add_view.instance_forms()
  367. def context(self, context):
  368. helper = FormHelper()
  369. helper.form_tag = False
  370. helper.include_media = False
  371. context.update({
  372. 'addform': self.add_view.form_obj,
  373. 'addhelper': helper,
  374. 'addurl': self.add_view.model_admin_url('add'),
  375. 'model': self.model
  376. })
  377. def media(self):
  378. return self.add_view.media + self.add_view.form_obj.media + self.vendor('xadmin.plugin.quick-form.js')
  379. class Dashboard(CommAdminView):
  380. widget_customiz = True
  381. widgets = []
  382. title = _(u"Dashboard")
  383. icon = None
  384. def get_page_id(self):
  385. return self.request.path
  386. def get_portal_key(self):
  387. return "dashboard:%s:pos" % self.get_page_id()
  388. @filter_hook
  389. def get_widget(self, widget_or_id, data=None):
  390. try:
  391. if isinstance(widget_or_id, UserWidget):
  392. widget = widget_or_id
  393. else:
  394. widget = UserWidget.objects.get(user=self.user, page_id=self.get_page_id(), id=widget_or_id)
  395. wid = widget_manager.get(widget.widget_type)
  396. class widget_with_perm(wid):
  397. def context(self, context):
  398. super(widget_with_perm, self).context(context)
  399. context.update({'has_change_permission': self.request.user.has_perm('xadmin.change_userwidget')})
  400. wid_instance = widget_with_perm(self, data or widget.get_value())
  401. return wid_instance
  402. except UserWidget.DoesNotExist:
  403. return None
  404. @filter_hook
  405. def get_init_widget(self):
  406. portal = []
  407. widgets = self.widgets
  408. for col in widgets:
  409. portal_col = []
  410. for opts in col:
  411. try:
  412. widget = UserWidget(user=self.user, page_id=self.get_page_id(), widget_type=opts['type'])
  413. widget.set_value(opts)
  414. widget.save()
  415. portal_col.append(self.get_widget(widget))
  416. except (PermissionDenied, WidgetDataError):
  417. widget.delete()
  418. continue
  419. portal.append(portal_col)
  420. UserSettings(
  421. user=self.user, key="dashboard:%s:pos" % self.get_page_id(),
  422. value='|'.join([','.join([str(w.id) for w in col]) for col in portal])).save()
  423. return portal
  424. @filter_hook
  425. def get_widgets(self):
  426. if self.widget_customiz:
  427. portal_pos = UserSettings.objects.filter(
  428. user=self.user, key=self.get_portal_key())
  429. if len(portal_pos):
  430. portal_pos = portal_pos[0].value
  431. widgets = []
  432. if portal_pos:
  433. user_widgets = dict([(uw.id, uw) for uw in UserWidget.objects.filter(user=self.user, page_id=self.get_page_id())])
  434. for col in portal_pos.split('|'):
  435. ws = []
  436. for wid in col.split(','):
  437. try:
  438. widget = user_widgets.get(int(wid))
  439. if widget:
  440. ws.append(self.get_widget(widget))
  441. except Exception as e:
  442. import logging
  443. logging.error(e, exc_info=True)
  444. widgets.append(ws)
  445. return widgets
  446. return self.get_init_widget()
  447. @filter_hook
  448. def get_title(self):
  449. return self.title
  450. @filter_hook
  451. def get_context(self):
  452. new_context = {
  453. 'title': self.get_title(),
  454. 'icon': self.icon,
  455. 'portal_key': self.get_portal_key(),
  456. 'columns': [('col-sm-%d' % int(12 / len(self.widgets)), ws) for ws in self.widgets],
  457. 'has_add_widget_permission': self.has_model_perm(UserWidget, 'add') and self.widget_customiz,
  458. 'add_widget_url': self.get_admin_url('%s_%s_add' % (UserWidget._meta.app_label, UserWidget._meta.model_name)) +
  459. "?user=%s&page_id=%s&_redirect=%s" % (self.user.id, self.get_page_id(), urlquote(self.request.get_full_path()))
  460. }
  461. context = super(Dashboard, self).get_context()
  462. context.update(new_context)
  463. return context
  464. @never_cache
  465. def get(self, request, *args, **kwargs):
  466. self.widgets = self.get_widgets()
  467. return self.template_response('xadmin/views/dashboard.html', self.get_context())
  468. @csrf_protect_m
  469. def post(self, request, *args, **kwargs):
  470. if 'id' in request.POST:
  471. widget_id = request.POST['id']
  472. if request.POST.get('_delete', None) != 'on':
  473. widget = self.get_widget(widget_id, request.POST.copy())
  474. widget.save()
  475. else:
  476. try:
  477. widget = UserWidget.objects.get(
  478. user=self.user, page_id=self.get_page_id(), id=widget_id)
  479. widget.delete()
  480. try:
  481. portal_pos = UserSettings.objects.get(user=self.user, key="dashboard:%s:pos" % self.get_page_id())
  482. pos = [[w for w in col.split(',') if w != str(
  483. widget_id)] for col in portal_pos.value.split('|')]
  484. portal_pos.value = '|'.join([','.join(col) for col in pos])
  485. portal_pos.save()
  486. except Exception:
  487. pass
  488. except UserWidget.DoesNotExist:
  489. pass
  490. return self.get(request)
  491. @filter_hook
  492. def get_media(self):
  493. media = super(Dashboard, self).get_media() + \
  494. self.vendor('xadmin.page.dashboard.js', 'xadmin.page.dashboard.css')
  495. if self.widget_customiz:
  496. media = media + self.vendor('xadmin.plugin.portal.js')
  497. for ws in self.widgets:
  498. for widget in ws:
  499. media = media + widget.media()
  500. return media
  501. class ModelDashboard(Dashboard, ModelAdminView):
  502. title = _(u"%s Dashboard")
  503. def get_page_id(self):
  504. return 'model:%s/%s' % self.model_info
  505. @filter_hook
  506. def get_title(self):
  507. return self.title % force_text(self.obj)
  508. def init_request(self, object_id, *args, **kwargs):
  509. self.obj = self.get_object(unquote(object_id))
  510. if not self.has_view_permission(self.obj):
  511. raise PermissionDenied
  512. if self.obj is None:
  513. raise Http404(_('%(name)s object with primary key %(key)r does not exist.') %
  514. {'name': force_text(self.opts.verbose_name), 'key': escape(object_id)})
  515. @filter_hook
  516. def get_context(self):
  517. new_context = {
  518. 'has_change_permission': self.has_change_permission(self.obj),
  519. 'object': self.obj,
  520. }
  521. context = Dashboard.get_context(self)
  522. context.update(ModelAdminView.get_context(self))
  523. context.update(new_context)
  524. return context
  525. @never_cache
  526. def get(self, request, *args, **kwargs):
  527. self.widgets = self.get_widgets()
  528. return self.template_response(self.get_template_list('views/model_dashboard.html'), self.get_context())