modules.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import json
  2. from django import forms
  3. from django.contrib.admin.models import LogEntry
  4. from django.db.models import Q
  5. from django.template.loader import render_to_string
  6. from django.utils.translation import ugettext_lazy as _
  7. from jet.utils import get_app_list, LazyDateTimeEncoder, context_to_dict
  8. import datetime
  9. class DashboardModule(object):
  10. """
  11. Base dashboard module class. All dashboard modules (widgets) should inherit it.
  12. """
  13. #: Path to widget's template. There is no need to extend such templates from any base templates.
  14. template = 'jet.dashboard/module.html'
  15. enabled = True
  16. #: Specify if module can be draggable or has static position.
  17. draggable = True
  18. #: Specify if module can be collapsed.
  19. collapsible = True
  20. #: Specify if module can be deleted.
  21. deletable = True
  22. show_title = True
  23. #: Default widget title that will be displayed for widget in the dashboard. User can change it later
  24. #: for every widget.
  25. title = ''
  26. #: Specify title url. ``None`` if title shouldn't be clickable.
  27. title_url = None
  28. css_classes = None
  29. #: HTML content that will be displayed before widget content.
  30. pre_content = None
  31. #: HTML content that will be displayed after widget content.
  32. post_content = None
  33. children = None
  34. #: A ``django.forms.Form`` class which may contain custom widget settings. Not required.
  35. settings_form = None
  36. #: A ``django.forms.Form`` class which may contain custom widget child settings, if it has any. Not required.
  37. child_form = None
  38. #: Child name that will be displayed when editing module contents. Required if ``child_form`` set.
  39. child_name = None
  40. #: Same as child name, but plural.
  41. child_name_plural = None
  42. settings = None
  43. column = None
  44. order = None
  45. #: A boolean field which specify if widget should be rendered on dashboard page load or fetched
  46. #: later via AJAX.
  47. ajax_load = False
  48. #: A boolean field which makes widget ui color contrast.
  49. contrast = False
  50. #: Optional style attributes which will be applied to widget content container.
  51. style = False
  52. class Media:
  53. css = ()
  54. js = ()
  55. def __init__(self, title=None, model=None, context=None, **kwargs):
  56. if title is not None:
  57. self.title = title
  58. self.model = model
  59. self.context = context or {}
  60. for key in kwargs:
  61. if hasattr(self.__class__, key):
  62. setattr(self, key, kwargs[key])
  63. self.children = self.children or []
  64. if self.model:
  65. self.load_from_model()
  66. def fullname(self):
  67. return self.__module__ + "." + self.__class__.__name__
  68. def load_settings(self, settings):
  69. """
  70. Should be implemented to restore saved in database settings. Required if you have custom settings.
  71. """
  72. pass
  73. def load_children(self, children):
  74. self.children = children
  75. def store_children(self):
  76. """
  77. Specify if children field should be saved to database.
  78. """
  79. return False
  80. def settings_dict(self):
  81. """
  82. Should be implemented to save settings to database. This method should return ``dict`` which will be serialized
  83. using ``json``. Required if you have custom settings.
  84. """
  85. pass
  86. def dump_settings(self, settings=None):
  87. settings = settings or self.settings_dict()
  88. if settings:
  89. return json.dumps(settings, cls=LazyDateTimeEncoder)
  90. else:
  91. return ''
  92. def dump_children(self):
  93. if self.store_children():
  94. return json.dumps(self.children, cls=LazyDateTimeEncoder)
  95. else:
  96. return ''
  97. def load_from_model(self):
  98. self.title = self.model.title
  99. if self.model.settings:
  100. try:
  101. self.settings = json.loads(self.model.settings)
  102. self.load_settings(self.settings)
  103. except ValueError:
  104. pass
  105. if self.store_children() and self.model.children:
  106. try:
  107. children = json.loads(self.model.children)
  108. self.load_children(children)
  109. except ValueError:
  110. pass
  111. def init_with_context(self, context):
  112. """
  113. Allows you to load data and initialize module's state.
  114. """
  115. pass
  116. def get_context_data(self):
  117. context = context_to_dict(self.context)
  118. context.update({
  119. 'module': self
  120. })
  121. return context
  122. def render(self):
  123. self.init_with_context(self.context)
  124. return render_to_string(self.template, self.get_context_data())
  125. class LinkListItemForm(forms.Form):
  126. url = forms.CharField(label=_('URL'))
  127. title = forms.CharField(label=_('Title'))
  128. external = forms.BooleanField(label=_('External link'), required=False)
  129. class LinkListSettingsForm(forms.Form):
  130. layout = forms.ChoiceField(label=_('Layout'), choices=(('stacked', _('Stacked')), ('inline', _('Inline'))))
  131. class LinkList(DashboardModule):
  132. """
  133. List of links widget.
  134. Usage example:
  135. .. code-block:: python
  136. from django.utils.translation import ugettext_lazy as _
  137. from jet.dashboard import modules
  138. from jet.dashboard.dashboard import Dashboard, AppIndexDashboard
  139. class CustomIndexDashboard(Dashboard):
  140. columns = 3
  141. def init_with_context(self, context):
  142. self.available_children.append(modules.LinkList)
  143. self.children.append(modules.LinkList(
  144. _('Support'),
  145. children=[
  146. {
  147. 'title': _('Django documentation'),
  148. 'url': 'http://docs.djangoproject.com/',
  149. 'external': True,
  150. },
  151. {
  152. 'title': _('Django "django-users" mailing list'),
  153. 'url': 'http://groups.google.com/group/django-users',
  154. 'external': True,
  155. },
  156. {
  157. 'title': _('Django irc channel'),
  158. 'url': 'irc://irc.freenode.net/django',
  159. 'external': True,
  160. },
  161. ],
  162. column=0,
  163. order=0
  164. ))
  165. """
  166. title = _('Links')
  167. template = 'jet.dashboard/modules/link_list.html'
  168. #: Specify widget layout.
  169. #: Allowed values ``stacked`` and ``inline``.
  170. layout = 'stacked'
  171. #: Links are contained in ``children`` attribute which you can pass as constructor parameter
  172. #: to make your own preinstalled link lists.
  173. #:
  174. #: ``children`` is an array of dictinaries::
  175. #:
  176. #: [
  177. #: {
  178. #: 'title': _('Django documentation'),
  179. #: 'url': 'http://docs.djangoproject.com/',
  180. #: 'external': True,
  181. #: },
  182. #: ...
  183. #: ]
  184. children = []
  185. settings_form = LinkListSettingsForm
  186. child_form = LinkListItemForm
  187. child_name = _('Link')
  188. child_name_plural = _('Links')
  189. def __init__(self, title=None, children=list(), **kwargs):
  190. children = list(map(self.parse_link, children))
  191. kwargs.update({'children': children})
  192. super(LinkList, self).__init__(title, **kwargs)
  193. def settings_dict(self):
  194. return {
  195. 'draggable': self.draggable,
  196. 'deletable': self.deletable,
  197. 'collapsible': self.collapsible,
  198. 'layout': self.layout
  199. }
  200. def load_settings(self, settings):
  201. self.draggable = settings.get('draggable', self.draggable)
  202. self.deletable = settings.get('deletable', self.deletable)
  203. self.collapsible = settings.get('collapsible', self.collapsible)
  204. self.layout = settings.get('layout', self.layout)
  205. def store_children(self):
  206. return True
  207. def parse_link(self, link):
  208. if isinstance(link, (tuple, list)):
  209. link_dict = {'title': link[0], 'url': link[1]}
  210. if len(link) >= 3:
  211. link_dict['external'] = link[2]
  212. return link_dict
  213. elif isinstance(link, (dict,)):
  214. return link
  215. class AppList(DashboardModule):
  216. """
  217. Shows applications and containing models links. For each model "created" and "change" links are displayed.
  218. Usage example:
  219. .. code-block:: python
  220. from django.utils.translation import ugettext_lazy as _
  221. from jet.dashboard import modules
  222. from jet.dashboard.dashboard import Dashboard, AppIndexDashboard
  223. class CustomIndexDashboard(Dashboard):
  224. columns = 3
  225. def init_with_context(self, context):
  226. self.children.append(modules.AppList(
  227. _('Applications'),
  228. exclude=('auth.*',),
  229. column=0,
  230. order=0
  231. ))
  232. """
  233. title = _('Applications')
  234. template = 'jet.dashboard/modules/app_list.html'
  235. #: Specify models which should be displayed. ``models`` is an array of string formatted as ``app_label.model``.
  236. #: Also its possible to specify all application models with * sign (e.g. ``auth.*``).
  237. models = None
  238. #: Specify models which should NOT be displayed. ``exclude`` is an array of string formatted as ``app_label.model``.
  239. #: Also its possible to specify all application models with * sign (e.g. ``auth.*``).
  240. exclude = None
  241. hide_empty = True
  242. def settings_dict(self):
  243. return {
  244. 'models': self.models,
  245. 'exclude': self.exclude
  246. }
  247. def load_settings(self, settings):
  248. self.models = settings.get('models')
  249. self.exclude = settings.get('exclude')
  250. def init_with_context(self, context):
  251. app_list = get_app_list(context)
  252. app_to_remove = []
  253. for app in app_list:
  254. app_name = app.get('app_label', app.get('name', ''))
  255. app['models'] = filter(
  256. lambda model: self.models is None or ('%s.%s' % (app_name, model['object_name'])) in self.models or ('%s.*' % app_name) in self.models,
  257. app['models']
  258. )
  259. app['models'] = filter(
  260. lambda model: self.exclude is None or (('%s.%s' % (app_name, model['object_name'])) not in self.exclude and ('%s.*' % app_name) not in self.exclude),
  261. app['models']
  262. )
  263. app['models'] = list(app['models'])
  264. if self.hide_empty and len(list(app['models'])) == 0:
  265. app_to_remove.append(app)
  266. for app in app_to_remove:
  267. app_list.remove(app)
  268. self.children = app_list
  269. class ModelList(DashboardModule):
  270. """
  271. Shows models links. For each model "created" and "change" links are displayed.
  272. Usage example:
  273. .. code-block:: python
  274. from django.utils.translation import ugettext_lazy as _
  275. from jet.dashboard import modules
  276. from jet.dashboard.dashboard import Dashboard, AppIndexDashboard
  277. class CustomIndexDashboard(Dashboard):
  278. columns = 3
  279. def init_with_context(self, context):
  280. self.children.append(modules.ModelList(
  281. _('Models'),
  282. exclude=('auth.*',),
  283. column=0,
  284. order=0
  285. ))
  286. """
  287. title = _('Models')
  288. template = 'jet.dashboard/modules/model_list.html'
  289. #: Specify models which should be displayed. ``models`` is an array of string formatted as ``app_label.model``.
  290. #: Also its possible to specify all application models with * sign (e.g. ``auth.*``).
  291. models = None
  292. #: Specify models which should NOT be displayed. ``exclude`` is an array of string formatted as ``app_label.model``.
  293. #: Also its possible to specify all application models with * sign (e.g. ``auth.*``).
  294. exclude = None
  295. hide_empty = True
  296. def settings_dict(self):
  297. return {
  298. 'models': self.models,
  299. 'exclude': self.exclude
  300. }
  301. def load_settings(self, settings):
  302. self.models = settings.get('models')
  303. self.exclude = settings.get('exclude')
  304. def init_with_context(self, context):
  305. app_list = get_app_list(context)
  306. models = []
  307. for app in app_list:
  308. app_name = app.get('app_label', app.get('name', ''))
  309. app['models'] = filter(
  310. lambda model: self.models is None or ('%s.%s' % (app_name, model['object_name'])) in self.models or ('%s.*' % app_name) in self.models,
  311. app['models']
  312. )
  313. app['models'] = filter(
  314. lambda model: self.exclude is None or (('%s.%s' % (app_name, model['object_name'])) not in self.exclude and ('%s.*' % app_name) not in self.exclude),
  315. app['models']
  316. )
  317. app['models'] = list(app['models'])
  318. models.extend(app['models'])
  319. self.children = models
  320. class RecentActionsSettingsForm(forms.Form):
  321. limit = forms.IntegerField(label=_('Items limit'), min_value=1)
  322. class RecentActions(DashboardModule):
  323. """
  324. Display list of most recent admin actions with following information:
  325. entity name, type of action, author, date
  326. Usage example:
  327. .. code-block:: python
  328. from django.utils.translation import ugettext_lazy as _
  329. from jet.dashboard import modules
  330. from jet.dashboard.dashboard import Dashboard, AppIndexDashboard
  331. class CustomIndexDashboard(Dashboard):
  332. columns = 3
  333. def init_with_context(self, context):
  334. self.children.append(modules.RecentActions(
  335. _('Recent Actions'),
  336. 10,
  337. column=0,
  338. order=0
  339. ))
  340. """
  341. title = _('Recent Actions')
  342. template = 'jet.dashboard/modules/recent_actions.html'
  343. #: Number if entries to be shown (may be changed by each user personally).
  344. limit = 10
  345. #: Specify actions of which models should be displayed. ``include_list`` is an array of string
  346. #: formatted as ``app_label.model``. Also its possible to specify all application models
  347. #: with * sign (e.g. ``auth.*``).
  348. include_list = None
  349. #: Specify actions of which models should NOT be displayed. ``exclude_list`` is an array of string
  350. #: formatted as ``app_label.model``. Also its possible to specify all application models
  351. #: with * sign (e.g. ``auth.*``).
  352. exclude_list = None
  353. settings_form = RecentActionsSettingsForm
  354. user = None
  355. def __init__(self, title=None, limit=10, **kwargs):
  356. kwargs.update({'limit': limit})
  357. super(RecentActions, self).__init__(title, **kwargs)
  358. def settings_dict(self):
  359. return {
  360. 'limit': self.limit,
  361. 'include_list': self.include_list,
  362. 'exclude_list': self.exclude_list,
  363. 'user': self.user
  364. }
  365. def load_settings(self, settings):
  366. self.limit = settings.get('limit', self.limit)
  367. self.include_list = settings.get('include_list')
  368. self.exclude_list = settings.get('exclude_list')
  369. self.user = settings.get('user', None)
  370. def init_with_context(self, context):
  371. def get_qset(list):
  372. qset = None
  373. for contenttype in list:
  374. try:
  375. app_label, model = contenttype.split('.')
  376. if model == '*':
  377. current_qset = Q(
  378. content_type__app_label=app_label
  379. )
  380. else:
  381. current_qset = Q(
  382. content_type__app_label=app_label,
  383. content_type__model=model
  384. )
  385. except:
  386. raise ValueError('Invalid contenttype: "%s"' % contenttype)
  387. if qset is None:
  388. qset = current_qset
  389. else:
  390. qset = qset | current_qset
  391. return qset
  392. qs = LogEntry.objects
  393. if self.user:
  394. qs = qs.filter(
  395. user__pk=int(self.user)
  396. )
  397. if self.include_list:
  398. qs = qs.filter(get_qset(self.include_list))
  399. if self.exclude_list:
  400. qs = qs.exclude(get_qset(self.exclude_list))
  401. self.children = qs.select_related('content_type', 'user')[:int(self.limit)]
  402. class FeedSettingsForm(forms.Form):
  403. limit = forms.IntegerField(label=_('Items limit'), min_value=1)
  404. feed_url = forms.URLField(label=_('Feed URL'))
  405. class Feed(DashboardModule):
  406. """
  407. Display RSS Feed entries with following information:
  408. entry title, date and link to the full version
  409. Usage example:
  410. .. code-block:: python
  411. from django.utils.translation import ugettext_lazy as _
  412. from jet.dashboard import modules
  413. from jet.dashboard.dashboard import Dashboard, AppIndexDashboard
  414. class CustomIndexDashboard(Dashboard):
  415. columns = 3
  416. def init_with_context(self, context):
  417. self.children.append(modules.Feed(
  418. _('Latest Django News'),
  419. feed_url='http://www.djangoproject.com/rss/weblog/',
  420. limit=5,
  421. column=0,
  422. order=0
  423. ))
  424. """
  425. title = _('RSS Feed')
  426. template = 'jet.dashboard/modules/feed.html'
  427. #: URL of the RSS feed (may be changed by each user personally).
  428. feed_url = None
  429. #: Number if entries to be shown (may be changed by each user personally).
  430. limit = None
  431. settings_form = FeedSettingsForm
  432. ajax_load = True
  433. def __init__(self, title=None, feed_url=None, limit=None, **kwargs):
  434. kwargs.update({'feed_url': feed_url, 'limit': limit})
  435. super(Feed, self).__init__(title, **kwargs)
  436. def settings_dict(self):
  437. return {
  438. 'feed_url': self.feed_url,
  439. 'limit': self.limit
  440. }
  441. def load_settings(self, settings):
  442. self.feed_url = settings.get('feed_url')
  443. self.limit = settings.get('limit')
  444. def init_with_context(self, context):
  445. if self.feed_url is not None:
  446. try:
  447. import feedparser
  448. feed = feedparser.parse(self.feed_url)
  449. if self.limit is not None:
  450. entries = feed['entries'][:self.limit]
  451. else:
  452. entries = feed['entries']
  453. for entry in entries:
  454. try:
  455. entry.date = datetime.date(*entry.published_parsed[0:3])
  456. except:
  457. pass
  458. self.children.append(entry)
  459. except ImportError:
  460. self.children.append({
  461. 'title': _('You must install the FeedParser python module'),
  462. 'warning': True,
  463. })
  464. else:
  465. self.children.append({
  466. 'title': _('You must provide a valid feed URL'),
  467. 'warning': True,
  468. })