# Copyright (c) 2011, 2012 Free Software Foundation # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # This project incorporates work covered by the following copyright and permission notice: # Copyright (c) 2009, Julien Fache # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of the author nor the names of other # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # Copyright (c) 2011, 2012 Free Software Foundation # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """ Module where grappelli dashboard modules classes are defined. """ # DJANGO IMPORTS from django.utils.text import capfirst from django.core.urlresolvers import reverse from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from django.utils.itercompat import is_iterable # GRAPPELL IMPORTS from grappelli.dashboard.utils import AppListElementMixin class DashboardModule(object): """ Base class for all dashboard modules. Dashboard modules have the following properties: ``collapsible`` Boolean that determines whether the module is collapsible, this allows users to show/hide module content. Default: ``True``. ``column`` Integer that corresponds to the column. Default: None. ``title`` String that contains the module title, make sure you use the django gettext functions if your application is multilingual. Default value: ''. ``title_url`` String that contains the module title URL. If given the module title will be a link to this URL. Default value: ``None``. ``css_classes`` A list of css classes to be added to the module ``div`` class attribute. Default value: ``None``. ``pre_content`` Text or HTML content to display above the module content. Default value: ``None``. ``content`` The module text or HTML content. Default value: ``None``. ``post_content`` Text or HTML content to display under the module content. Default value: ``None``. ``template`` The template to use to render the module. Default value: 'grappelli/dashboard/module.html'. """ template = 'grappelli/dashboard/module.html' collapsible = True column = None show_title = True title = '' title_url = None css_classes = None pre_content = None post_content = None children = None def __init__(self, title=None, **kwargs): if title is not None: self.title = title for key in kwargs: if hasattr(self.__class__, key): setattr(self, key, kwargs[key]) self.children = self.children or [] self.css_classes = self.css_classes or [] # boolean flag to ensure that the module is initialized only once self._initialized = False def init_with_context(self, context): """ Like for the :class:`~grappelli.dashboard.Dashboard` class, dashboard modules have a ``init_with_context`` method that is called with a ``django.template.RequestContext`` instance as unique argument. This gives you enough flexibility to build complex modules, for example, let's build a "history" dashboard module, that will list the last ten visited pages:: from grappelli.dashboard import modules class HistoryDashboardModule(modules.LinkList): title = 'History' def init_with_context(self, context): request = context['request'] # we use sessions to store the visited pages stack history = request.session.get('history', []) for item in history: self.children.append(item) # add the current page to the history history.insert(0, { 'title': context['title'], 'url': request.META['PATH_INFO'] }) if len(history) > 10: history = history[:10] request.session['history'] = history """ pass def is_empty(self): """ Return True if the module has no content and False otherwise. """ return self.pre_content is None and \ self.post_content is None and \ len(self.children) == 0 def render_css_classes(self): """ Return a string containing the css classes for the module. """ ret = ['dashboard-module'] if self.collapsible: ret.append('collapsible') ret += self.css_classes return ' '.join(ret) class Group(DashboardModule): """ Represents a group of modules. Here's an example of modules group:: from grappelli.dashboard import modules, Dashboard class MyDashboard(Dashboard): def __init__(self, **kwargs): Dashboard.__init__(self, **kwargs) self.children.append(modules.Group( title="My group", children=[ modules.AppList( title='Administration', models=('django.contrib.*',) ), modules.AppList( title='Applications', exclude=('django.contrib.*',) ) ] )) """ template = 'grappelli/dashboard/modules/group.html' def init_with_context(self, context): if self._initialized: return for module in self.children: module.init_with_context(context) self._initialized = True def is_empty(self): """ A group of modules is considered empty if it has no children or if all its children are empty. """ if super(Group, self).is_empty(): return True for child in self.children: if not child.is_empty(): return False return True class LinkList(DashboardModule): """ A module that displays a list of links. """ title = _('Links') template = 'grappelli/dashboard/modules/link_list.html' def init_with_context(self, context): if self._initialized: return new_children = [] for link in self.children: if isinstance(link, (tuple, list,)): link_dict = {'title': link[0], 'url': link[1]} if len(link) >= 3: link_dict['external'] = link[2] if len(link) >= 4: link_dict['description'] = link[3] new_children.append(link_dict) else: new_children.append(link) self.children = new_children self._initialized = True class AppList(DashboardModule, AppListElementMixin): """ Module that lists installed apps and their models. """ title = _('Applications') template = 'grappelli/dashboard/modules/app_list.html' models = None exclude = None def __init__(self, title=None, **kwargs): self.models = list(kwargs.pop('models', [])) self.exclude = list(kwargs.pop('exclude', [])) super(AppList, self).__init__(title, **kwargs) def init_with_context(self, context): if self._initialized: return items = self._visible_models(context['request']) apps = {} for model, perms in items: app_label = model._meta.app_label if app_label not in apps: apps[app_label] = { 'title': capfirst(app_label.title()), 'url': self._get_admin_app_list_url(model, context), 'models': [] } model_dict = {} model_dict['title'] = capfirst(model._meta.verbose_name_plural) if perms['change']: model_dict['change_url'] = self._get_admin_change_url(model, context) if perms['add']: model_dict['add_url'] = self._get_admin_add_url(model, context) apps[app_label]['models'].append(model_dict) apps_sorted = apps.keys() apps_sorted.sort() for app in apps_sorted: # sort model list alphabetically apps[app]['models'].sort(lambda x, y: cmp(x['title'], y['title'])) self.children.append(apps[app]) self._initialized = True class ModelList(DashboardModule, AppListElementMixin): """ Module that lists a set of models. """ template = 'grappelli/dashboard/modules/model_list.html' models = None exclude = None def __init__(self, title=None, models=None, exclude=None, **kwargs): self.models = list(models or []) self.exclude = list(exclude or []) super(ModelList, self).__init__(title, **kwargs) def init_with_context(self, context): if self._initialized: return items = self._visible_models(context['request']) if not items: return for model, perms in items: model_dict = {} model_dict['title'] = capfirst(model._meta.verbose_name_plural) if perms['change']: model_dict['change_url'] = self._get_admin_change_url(model, context) if perms['add']: model_dict['add_url'] = self._get_admin_add_url(model, context) self.children.append(model_dict) self._initialized = True class RecentActions(DashboardModule): """ Module that lists the recent actions for the current user. """ title = _('Recent Actions') template = 'grappelli/dashboard/modules/recent_actions.html' limit = 10 include_list = None exclude_list = None def __init__(self, title=None, limit=10, include_list=None, exclude_list=None, **kwargs): self.include_list = include_list or [] self.exclude_list = exclude_list or [] kwargs.update({'limit': limit}) super(RecentActions, self).__init__(title, **kwargs) def init_with_context(self, context): if self._initialized: return from django.db.models import Q from django.contrib.admin.models import LogEntry request = context['request'] def get_qset(list): qset = None for contenttype in list: if isinstance(contenttype, ContentType): current_qset = Q(content_type__id=contenttype.id) else: try: app_label, model = contenttype.split('.') except: raise ValueError('Invalid contenttype: "%s"' % contenttype) current_qset = Q( content_type__app_label=app_label, content_type__model=model ) if qset is None: qset = current_qset else: qset = qset | current_qset return qset if request.user is None: qs = LogEntry.objects.all() else: qs = LogEntry.objects.filter(user__id__exact=request.user.id) if self.include_list: qs = qs.filter(get_qset(self.include_list)) if self.exclude_list: qs = qs.exclude(get_qset(self.exclude_list)) self.children = qs.select_related('content_type', 'user')[:self.limit] if not len(self.children): self.pre_content = _('No recent actions.') self._initialized = True class Feed(DashboardModule): """ Class that represents a feed dashboard module. """ title = _('RSS Feed') template = 'grappelli/dashboard/modules/feed.html' feed_url = None limit = None def __init__(self, title=None, feed_url=None, limit=None, **kwargs): kwargs.update({'feed_url': feed_url, 'limit': limit}) super(Feed, self).__init__(title, **kwargs) def init_with_context(self, context): if self._initialized: return import datetime if self.feed_url is None: raise ValueError('You must provide a valid feed URL') try: import feedparser except ImportError: self.children.append({ 'title': ('You must install the FeedParser python module'), 'warning': True, }) return feed = feedparser.parse(self.feed_url) if self.limit is not None: entries = feed['entries'][:self.limit] else: entries = feed['entries'] for entry in entries: entry.url = entry.link try: entry.date = datetime.date(*entry.updated_parsed[0:3]) except: # no date for certain feeds pass self.children.append(entry) self._initialized = True