diff --git a/mayan/apps/checkouts/events.py b/mayan/apps/checkouts/events.py index 747da656df..e402f0faa2 100644 --- a/mayan/apps/checkouts/events.py +++ b/mayan/apps/checkouts/events.py @@ -5,36 +5,6 @@ from django.utils.translation import ugettext_lazy as _ from events.classes import Event event_document_check_out = Event(name='checkouts_document_check_out', label=_('Document checked out')) - -HISTORY_DOCUMENT_CHECKED_OUT = { - 'namespace': 'checkouts', 'name': 'document_checked_out', - 'label': _(u'Document checked out'), - 'summary': _(u'Document "%(document)s" checked out by %(fullname)s.'), - 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user'} -} - event_document_check_in = Event(name='checkouts_document_check_in', label=_('Document checked in')) - -HISTORY_DOCUMENT_CHECKED_IN = { - 'namespace': 'checkouts', 'name': 'document_checked_in', - 'label': _(u'Document checked in'), - 'summary': _(u'Document "%(document)s" checked in by %(fullname)s.'), - 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user'} -} - event_document_auto_check_in = Event(name='checkouts_document_auto_check_in', label=_('Document automatically checked in')) - -HISTORY_DOCUMENT_AUTO_CHECKED_IN = { - 'namespace': 'checkouts', 'name': 'document_auto_checked_in', - 'label': _(u'Document automatically checked in'), - 'summary': _(u'Document "%(document)s" automatically checked in.'), -} - event_document_forceful_check_in = Event(name='checkouts_document_forceful_check_in', label=_('Document forcefully checked in')) - -HISTORY_DOCUMENT_FORCEFUL_CHECK_IN = { - 'namespace': 'checkouts', 'name': 'document_forefull_check_in', - 'label': _(u'Document forcefully checked in'), - 'summary': _(u'Document "%(document)s" forcefully checked in by %(fullname)s.'), - 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user'} -} diff --git a/mayan/apps/documents/__init__.py b/mayan/apps/documents/__init__.py index 16f968e6a3..1314fd3ce3 100644 --- a/mayan/apps/documents/__init__.py +++ b/mayan/apps/documents/__init__.py @@ -8,7 +8,7 @@ from acls.api import class_permissions from common.classes import ModelAttribute from common.utils import encapsulate, validate_path from dynamic_search.classes import SearchModel -from history.permissions import PERMISSION_HISTORY_VIEW +from events.permissions import PERMISSION_EVENTS_VIEW from main.api import register_maintenance_links from navigation.api import (register_links, register_model_list_columns) from navigation.links import link_spacer @@ -21,8 +21,9 @@ from .links import (document_clear_image_cache, document_clear_transformations, document_content, document_delete, document_document_type_edit, + document_events_view, document_multiple_document_type_edit, document_download, - document_edit, document_history_view, document_list, + document_edit, document_list, document_list_recent, document_multiple_delete, document_multiple_clear_transformations, document_multiple_download, @@ -72,8 +73,7 @@ register_links([Document], [document_multiple_clear_transformations, document_mu register_links(Document, [document_preview], menu_name='form_header', position=0) register_links(Document, [document_content], menu_name='form_header', position=1) register_links(Document, [document_properties], menu_name='form_header', position=2) -register_links(Document, [document_history_view], menu_name='form_header') -register_links(Document, [document_version_list], menu_name='form_header') +register_links(Document, [document_events_view, document_version_list], menu_name='form_header') # Document Version links register_links(DocumentVersion, [document_version_revert, document_version_download]) @@ -124,7 +124,7 @@ class_permissions(Document, [PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_TRANSFORM, PERMISSION_DOCUMENT_VERSION_REVERT, PERMISSION_DOCUMENT_VIEW, - PERMISSION_HISTORY_VIEW]) + PERMISSION_EVENTS_VIEW]) document_search = SearchModel('documents', 'Document', permission=PERMISSION_DOCUMENT_VIEW, serializer_string='documents.serializers.DocumentSerializer') diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 1e64964af0..86fbb8ef67 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -5,23 +5,4 @@ from django.utils.translation import ugettext_lazy as _ from events.classes import Event event_document_create = Event(name='documents_document_create', label=_('Document created')) - -HISTORY_DOCUMENT_CREATED = { - 'namespace': 'documents', 'name': 'document_created', - 'label': _(u'Document creation'), - 'summary': _(u'Document "%(content_object)s" created by %(fullname)s.'), - 'details': _(u'Document "%(content_object)s" created on %(datetime)s by %(fullname)s.'), - 'expressions': {'fullname': 'user[0]["fields"]["username"] if isinstance(user, list) else user.get_full_name() if user.get_full_name() else user.username'} -} - event_document_edited = Event(name='documents_document_edit', label=_('Document edited')) - -HISTORY_DOCUMENT_EDITED = { - 'namespace': 'documents', 'name': 'document_edited', - 'label': _(u'Document edited'), - 'summary': _(u'Document "%(content_object)s" edited by %(fullname)s.'), - 'details': _(u'Document "%(content_object)s" was edited on %(datetime)s by %(fullname)s.'), - 'expressions': { - 'fullname': 'user[0]["fields"]["username"] if isinstance(user, list) else user.get_full_name() if user.get_full_name() else user.username', - } -} diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 612a168ba5..68e518a647 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -2,7 +2,7 @@ from __future__ import absolute_import from django.utils.translation import ugettext_lazy as _ -from history.permissions import PERMISSION_HISTORY_VIEW +from events.permissions import PERMISSION_EVENTS_VIEW from .permissions import (PERMISSION_DOCUMENT_PROPERTIES_EDIT, PERMISSION_DOCUMENT_VIEW, PERMISSION_DOCUMENT_DELETE, @@ -54,7 +54,7 @@ document_multiple_update_page_count = {'text': _(u'Reset page count'), 'view': ' document_clear_transformations = {'text': _(u'Clear transformations'), 'view': 'documents:document_clear_transformations', 'args': 'object.id', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_multiple_clear_transformations = {'text': _(u'Clear transformations'), 'view': 'documents:document_multiple_clear_transformations', 'famfam': 'page_paintbrush', 'permissions': [PERMISSION_DOCUMENT_TRANSFORM]} document_print = {'text': _(u'Print'), 'view': 'documents:document_print', 'args': 'object.id', 'famfam': 'printer', 'permissions': [PERMISSION_DOCUMENT_VIEW]} -document_history_view = {'text': _(u'History'), 'view': 'history:history_for_object', 'args': ['"documents"', '"document"', 'object.id'], 'famfam': 'book_go', 'permissions': [PERMISSION_HISTORY_VIEW]} +document_events_view = {'text': _(u'Events'), 'view': 'events:events_for_object', 'args': ['"documents"', '"document"', 'object.id'], 'famfam': 'book_go', 'permissions': [PERMISSION_EVENTS_VIEW]} # Tools document_clear_image_cache = {'text': _(u'Clear the document image cache'), 'view': 'documents:document_clear_image_cache', 'famfam': 'camera_delete', 'permissions': [PERMISSION_DOCUMENT_TOOLS], 'description': _(u'Clear the graphics representations used to speed up the documents\' display and interactive transformations results.')} diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index 883d9c2c3f..0f64cb3c40 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -237,7 +237,7 @@ def document_edit(request, document_id): document.label = form.cleaned_data['document_type_available_filenames'].filename document.save() - event_document_edited.commit(actor=request.user) + event_document_edited.commit(actor=request.user, target=document) document.add_as_recent_document_for_user(request.user) messages.success(request, _(u'Document "%s" edited successfully.') % document) @@ -279,7 +279,7 @@ def document_document_type_edit(request, document_id=None, document_id_list=None for document in documents: document.set_document_type(form.cleaned_data['document_type']) - event_document_edited.commit(actor=request.user) + event_document_edited.commit(actor=request.user, target=document) document.add_as_recent_document_for_user(request.user) messages.success(request, _(u'Document type changed successfully.')) diff --git a/mayan/apps/events/__init__.py b/mayan/apps/events/__init__.py index 6a821ede7a..41c3555ac6 100644 --- a/mayan/apps/events/__init__.py +++ b/mayan/apps/events/__init__.py @@ -1 +1,8 @@ +from __future__ import absolute_import, unicode_literals + +from project_tools.api import register_tool + from .classes import Event # NOQA +from .links import events_list + +register_tool(events_list) diff --git a/mayan/apps/events/admin.py b/mayan/apps/events/admin.py index f22496a166..d3a218665d 100644 --- a/mayan/apps/events/admin.py +++ b/mayan/apps/events/admin.py @@ -2,8 +2,9 @@ from django.contrib import admin from .models import EventType + class EventTypeAdmin(admin.ModelAdmin): - readonly_fields = ('name', 'get_label') + readonly_fields = ('name', '__str__') admin.site.register(EventType, EventTypeAdmin) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index 222dc59558..38366dc37b 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -1,22 +1,31 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals -from django.core import serializers from django.db import models +from django.utils.translation import ugettext as _ from actstream import action class Event(object): + _labels = {} + + @classmethod + def get_label(cls, name): + try: + return cls._labels[name] + except KeyError: + return _('Unknown or obsolete event type: {0}'.format(name)) + def __init__(self, name, label): self.name = name self.label = label self.event_type = None + self.__class__._labels[name] = label def commit(self, actor=None, action_object=None, target=None): model = models.get_model('events', 'EventType') if not self.event_type: - self.event_type, created = model.objects.get_or_create( - label=self.label, name=self.name) + self.event_type, created = model.objects.get_or_create(name=self.name) action.send(actor or target, actor=actor, verb=self.name, action_object=action_object, target=target) diff --git a/mayan/apps/events/links.py b/mayan/apps/events/links.py new file mode 100644 index 0000000000..4692804e58 --- /dev/null +++ b/mayan/apps/events/links.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +events_list = {'text': _('Events'), 'view': 'events:events_list', 'famfam': 'book', 'icon': 'book.png'} diff --git a/mayan/apps/events/managers.py b/mayan/apps/events/managers.py deleted file mode 100644 index be2ed66f25..0000000000 --- a/mayan/apps/events/managers.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db import models - - -class EventTypeManager(models.Manager): - def create(self, *args, **kwargs): - label = kwargs.pop('label') - - instance = super(EventTypeManager, self).create(*args, **kwargs) - self.model._labels[instance.name] = label - return instance - - def get(self, *args, **kwargs): - instance = super(EventTypeManager, self).get(*args, **kwargs) - instance.label = self.model._labels[instance.name] - return instance - - def get_or_create(self, *args, **kwargs): - label = kwargs.pop('label') - - instance, boolean = super(EventTypeManager, self).get_or_create(*args, **kwargs) - self.model._labels[instance.name] = label - return instance, boolean diff --git a/mayan/apps/events/models.py b/mayan/apps/events/models.py index 57192c5d86..a7de6e58f3 100644 --- a/mayan/apps/events/models.py +++ b/mayan/apps/events/models.py @@ -4,30 +4,38 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ -from .managers import EventTypeManager +from actstream.models import Action + +from common.utils import encapsulate +from navigation.api import register_model_list_columns + +from .classes import Event +from .widgets import event_type_link @python_2_unicode_compatible class EventType(models.Model): - _labels = {} - name = models.CharField(max_length=64, unique=True, verbose_name=_('Name')) - objects = EventTypeManager() - def __str__(self): - return unicode(self.get_label()) - - def get_label(self): - try: - return self.__class__._labels[self.name] - except KeyError: - return _('Unknown or obsolete event type: {0}'.format(self.name)) - - #@models.permalink - #def get_absolute_url(self): - # return ('history_type_list', [self.pk]) + return unicode(Event.get_label(self.name)) class Meta: verbose_name = _('Event type') verbose_name_plural = _('Event types') + + +register_model_list_columns(Action, [ + { + 'name': _('Timestamp'), + 'attribute': 'timestamp' + }, + { + 'name': _('Actor'), + 'attribute': 'actor', + }, + { + 'name': _(u'Verb'), + 'attribute': encapsulate(lambda entry: event_type_link(entry)) + }, +]) diff --git a/mayan/apps/events/permissions.py b/mayan/apps/events/permissions.py new file mode 100644 index 0000000000..15c15b299f --- /dev/null +++ b/mayan/apps/events/permissions.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from permissions.models import PermissionNamespace, Permission + +events_namespace = PermissionNamespace('events', _('Events')) +PERMISSION_EVENTS_VIEW = Permission.objects.register(events_namespace, 'events_view', _('Access the events of an object')) diff --git a/mayan/apps/events/migrations/0001_initial.py b/mayan/apps/events/south_migrations/0001_initial.py similarity index 100% rename from mayan/apps/events/migrations/0001_initial.py rename to mayan/apps/events/south_migrations/0001_initial.py diff --git a/mayan/apps/events/south_migrations/0002_migrate_history_data.py b/mayan/apps/events/south_migrations/0002_migrate_history_data.py new file mode 100644 index 0000000000..df2de76d90 --- /dev/null +++ b/mayan/apps/events/south_migrations/0002_migrate_history_data.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +import json + +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +type_equivalence = { + 'documents.document_created': 'documents_document_create', + 'documents.document_edited': 'documents_document_edit', + 'checkouts.document_checked_out': 'checkouts_document_check_out', + 'checkouts.document_checked_in': 'checkouts_document_check_in', + 'checkouts.document_auto_checked_in': 'checkouts_document_auto_check_in', + 'checkouts.document_forefull_check_in': 'checkouts_document_forceful_check_in', +} + + +class Migration(DataMigration): + + depends_on = ( + ('actstream', '0001_initial'), + ('history', '0002_auto__chg_field_history_datetime'), + ) + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + + user_content_type = orm['contenttypes.contenttype'].objects.get(app_label='auth', model='user') + + for history_event in orm['history.history'].objects.all(): + user_pk = None + if history_event.dictionary: + loaded_dictionary = json.loads(history_event.dictionary) + if 'user' in loaded_dictionary: + user_pk = json.loads(loaded_dictionary['user']['value'])[0]['pk'] + + try: + orm['documents.document'].objects.get(pk=history_event.object_id) + except Exception: + pass + else: + if user_pk and history_event.object_id: + action = orm['actstream.action']( + timestamp=history_event.datetime, + actor_content_type=user_content_type, + actor_object_id=user_pk, + verb=type_equivalence['{0}.{1}'.format(history_event.history_type.namespace, history_event.history_type.name)], + target_content_type=history_event.content_type, + target_object_id=history_event.object_id + ) + action.save() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + u'actstream.action': { + 'Meta': {'ordering': "('-timestamp',)", 'object_name': 'Action'}, + 'action_object_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'action_object'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'action_object_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'actor_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'actor'", 'to': u"orm['contenttypes.ContentType']"}), + 'actor_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'target_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'target'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'target_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'verb': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'actstream.follow': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Follow'}, + 'actor_only': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'started': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'documents.document': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents'", 'to': u"orm['documents.DocumentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'default': "u'Uninitialized document'", 'max_length': '255', 'db_index': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "u'eng'", 'max_length': '8'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "u'bbce0d8c-5707-4b0d-a996-4f4780787b53'", 'max_length': '48'}) + }, + u'documents.documentpage': { + 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'}, + 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['documents.DocumentVersion']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + u'documents.documentpagetransformation': { + 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'}, + 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.DocumentPage']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + u'documents.documenttype': { + 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), + 'ocr': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'documents.documenttypefilename': { + 'Meta': {'ordering': "['filename']", 'unique_together': "(('document_type', 'filename'),)", 'object_name': 'DocumentTypeFilename'}, + 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'filenames'", 'to': u"orm['documents.DocumentType']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'documents.documentversion': { + 'Meta': {'object_name': 'DocumentVersion'}, + 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'versions'", 'to': u"orm['documents.Document']"}), + 'encoding': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + u'documents.recentdocument': { + 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'}, + 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.Document']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'events.eventtype': { + 'Meta': {'object_name': 'EventType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + u'history.history': { + 'Meta': {'ordering': "('-datetime',)", 'object_name': 'History'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'datetime': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'dictionary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'history_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['history.HistoryType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + u'history.historytype': { + 'Meta': {'ordering': "('namespace', 'name')", 'unique_together': "(('namespace', 'name'),)", 'object_name': 'HistoryType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'namespace': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + } + } + + complete_apps = ['history', 'auth', 'actstream', 'documents', 'events'] + symmetrical = True diff --git a/mayan/apps/events/migrations/__init__.py b/mayan/apps/events/south_migrations/__init__.py similarity index 100% rename from mayan/apps/events/migrations/__init__.py rename to mayan/apps/events/south_migrations/__init__.py diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py new file mode 100644 index 0000000000..e2c247db5c --- /dev/null +++ b/mayan/apps/events/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url + +urlpatterns = patterns('events.views', + url(r'^all/$', 'events_list', (), 'events_list'), + url(r'^for_object/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', 'events_list', (), 'events_for_object'), + url(r'^by_verb/(?P[\w\-]+)/$', 'events_list', (), 'events_by_verb'), +) diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 91ea44a218..b23198dc09 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -1,3 +1,88 @@ -from django.shortcuts import render +from __future__ import absolute_import, unicode_literals -# Create your views here. +from django.core.exceptions import PermissionDenied +from django.db.models.loading import get_model +from django.http import Http404 +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ + +from actstream.models import Action, any_stream + +from acls.models import AccessEntry +from common.utils import encapsulate +from permissions.models import Permission + +from .classes import Event +from .permissions import PERMISSION_EVENTS_VIEW +from .widgets import event_object_link + + +def events_list(request, app_label=None, module_name=None, object_id=None, verb=None): + extra_columns = [] + + context = { + 'extra_columns': extra_columns, + 'hide_object': True, + } + + if app_label and module_name and object_id: + model = get_model(app_label, module_name) + if not model: + raise Http404 + content_object = get_object_or_404(model, pk=object_id) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_EVENTS_VIEW]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_EVENTS_VIEW, request.user, content_object) + + context.update({ + 'object_list': any_stream(content_object), + 'title': _('Events for: %s') % content_object, + 'object': content_object + }) + elif verb: + pre_object_list = Action.objects.filter(verb=verb) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_EVENTS_VIEW]) + except PermissionDenied: + # If user doesn't have global permission, get a list of document + # for which he/she does hace access use it to filter the + # provided object_list + object_list = AccessEntry.objects.filter_objects_by_access(PERMISSION_EVENTS_VIEW, request.user, pre_object_list, related='content_object') + else: + object_list = pre_object_list + + context.update({ + 'title': _('Events of type: %s') % Event.get_label(verb), + 'object_list': object_list + }) + else: + pre_object_list = Action.objects.all() + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_EVENTS_VIEW]) + except PermissionDenied: + # If user doesn't have global permission, get a list of document + # for which he/she does hace access use it to filter the + # provided object_list + object_list = AccessEntry.objects.filter_objects_by_access(PERMISSION_EVENTS_VIEW, request.user, pre_object_list, related='content_object') + else: + object_list = pre_object_list + + context.update({ + 'title': _('Events'), + 'object_list': object_list + }) + + if not (app_label and module_name and object_id): + extra_columns.append( + { + 'name': _('Target'), + 'attribute': encapsulate(lambda entry: event_object_link(entry)) + } + ) + return render_to_response('main/generic_list.html', context, + context_instance=RequestContext(request)) diff --git a/mayan/apps/events/widgets.py b/mayan/apps/events/widgets.py new file mode 100644 index 0000000000..7ecb624007 --- /dev/null +++ b/mayan/apps/events/widgets.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe + +from .classes import Event + + +def event_object_link(entry): + return mark_safe('%(label)s' % { + 'url': entry.target.get_absolute_url() if entry.target else '#', + 'label': entry.target} + ) + + +def event_type_link(entry): + return mark_safe('%(label)s' % { + 'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}), + 'label': Event.get_label(entry.verb)} + ) diff --git a/mayan/apps/history/__init__.py b/mayan/apps/history/__init__.py index ce21b5edaa..8b13789179 100644 --- a/mayan/apps/history/__init__.py +++ b/mayan/apps/history/__init__.py @@ -1,30 +1 @@ -from __future__ import absolute_import -from django.utils.translation import ugettext_lazy as _ - -from project_tools.api import register_tool -from navigation.api import register_model_list_columns, register_links -from common.utils import encapsulate - -from .models import History -from .widgets import history_entry_type_link -from .links import history_list, history_details - -register_tool(history_list) - -register_model_list_columns(History, [ - { - 'name': _(u'Date and time'), - 'attribute': 'datetime' - }, - { - 'name': _(u'Type'), - 'attribute': encapsulate(lambda entry: history_entry_type_link(entry)) - }, - { - 'name': _(u'Summary'), - 'attribute': encapsulate(lambda entry: unicode(entry.get_processed_summary())) - } -]) - -register_links(History, [history_details]) diff --git a/mayan/apps/history/forms.py b/mayan/apps/history/forms.py deleted file mode 100644 index bac7cb503c..0000000000 --- a/mayan/apps/history/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import absolute_import - -from common.forms import DetailForm - -from .models import History - - -class HistoryDetailForm(DetailForm): - class Meta: - model = History - exclude = ('datetime', 'content_type', 'object_id', 'history_type', 'dictionary') diff --git a/mayan/apps/history/links.py b/mayan/apps/history/links.py deleted file mode 100644 index d70feec4ec..0000000000 --- a/mayan/apps/history/links.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import absolute_import - -from django.utils.translation import ugettext_lazy as _ - -from .permissions import PERMISSION_HISTORY_VIEW - -history_list = {'text': _(u'History'), 'view': 'history:history_list', 'famfam': 'book', 'icon': 'book.png'} -history_details = {'text': _(u'Details'), 'view': 'history:history_view', 'famfam': 'book_open', 'args': 'object.pk', 'permissions': [PERMISSION_HISTORY_VIEW]} diff --git a/mayan/apps/history/managers.py b/mayan/apps/history/managers.py deleted file mode 100644 index 3d69e7deaa..0000000000 --- a/mayan/apps/history/managers.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.core.urlresolvers import reverse -from django.db import models - - -class ObjectHistoryManager(models.Manager): - def get_url_for_object(self): - ct = ContentType.objects.get_for_model(self.instance) - return reverse('history_for_object', args=[ct, self.instance.pk]) diff --git a/mayan/apps/history/models.py b/mayan/apps/history/models.py index 8dda724f39..4167352687 100644 --- a/mayan/apps/history/models.py +++ b/mayan/apps/history/models.py @@ -1,108 +1,3 @@ -from __future__ import absolute_import - -import json -import pickle - -from django.contrib.contenttypes import generic -from django.contrib.contenttypes.models import ContentType -from django.core import serializers from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.core.urlresolvers import reverse - -#from .runtime_data import history_types_dict -class HistoryType(models.Model): - namespace = models.CharField(max_length=64, verbose_name=_(u'Namespace')) - name = models.CharField(max_length=64, verbose_name=_(u'Name')) - - def __unicode__(self): - return '{0}.{1}'.format(self.namespace, self.name) - # try: - # return unicode(history_types_dict[self.namespace][self.name]['label']) - # except KeyError: - # return u'Obsolete history type: %s - %s' % (self.namespace, self.name) - - def get_absolute_url(self): - return reverse('history:history_type_list', args=[self.pk]) - - class Meta: - ordering = ('namespace', 'name') - unique_together = ('namespace', 'name') - verbose_name = _(u'History type') - verbose_name_plural = _(u'History types') - - -class History(models.Model): - datetime = models.DateTimeField(verbose_name=_(u'Date time'), auto_now_add=True) - content_type = models.ForeignKey(ContentType, blank=True, null=True) - object_id = models.PositiveIntegerField(blank=True, null=True) - content_object = generic.GenericForeignKey('content_type', 'object_id') - history_type = models.ForeignKey(HistoryType, verbose_name=_(u'History type')) - dictionary = models.TextField(verbose_name=_(u'Dictionary'), blank=True) - - def __unicode__(self): - return u'%s - %s - %s' % (self.datetime, self.content_object, self.history_type) - - def get_label(self): - return history_types_dict[self.history_type.namespace][self.history_type.name]['label'] - - def get_summary(self): - return history_types_dict[self.history_type.namespace][self.history_type.name].get('summary', u'') - - def get_details(self): - return history_types_dict[self.history_type.namespace][self.history_type.name].get('details', u'') - - def get_expressions(self): - return history_types_dict[self.history_type.namespace][self.history_type.name].get('expressions', {}) - - def get_processed_summary(self): - return _process_history_text(self, self.get_summary()) - - def get_processed_details(self): - return _process_history_text(self, self.get_details()) - - @models.permalink - def get_absolute_url(self): - return ('history_view', [self.pk]) - - class Meta: - ordering = ('-datetime',) - verbose_name = _(u'History') - verbose_name_plural = _(u'Histories') - - -def _process_history_text(history, text): - key_values = { - 'content_object': history.content_object, - 'datetime': history.datetime - } - - loaded_dictionary = json.loads(history.dictionary) - - new_dict = {} - for key, values in loaded_dictionary.items(): - value_type = pickle.loads(str(values['type'])) - if isinstance(value_type, models.base.ModelBase): - for deserialized in serializers.deserialize('json', values['value']): - new_dict[key] = deserialized.object - elif isinstance(value_type, models.query.QuerySet): - qs = [] - for deserialized in serializers.deserialize('json', values['value']): - qs.append(deserialized.object) - new_dict[key] = qs - else: - new_dict[key] = json.loads(values['value']) - - key_values.update(new_dict) - expressions_dict = {} - - for key, value in history.get_expressions().items(): - try: - expressions_dict[key] = eval(value, key_values.copy()) - except Exception as exception: - expressions_dict[key] = exception - - key_values.update(expressions_dict) - return text % key_values diff --git a/mayan/apps/history/permissions.py b/mayan/apps/history/permissions.py deleted file mode 100644 index 7de8447eb6..0000000000 --- a/mayan/apps/history/permissions.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import absolute_import - -from django.utils.translation import ugettext_lazy as _ - -from permissions.models import PermissionNamespace, Permission - -history_namespace = PermissionNamespace('history', _(u'History')) -PERMISSION_HISTORY_VIEW = Permission.objects.register(history_namespace, 'history_view', _(u'Access the history of an object')) diff --git a/mayan/apps/history/south_migrations/0003_auto__del_history__del_historytype__del_unique_historytype_namespace_n.py b/mayan/apps/history/south_migrations/0003_auto__del_history__del_historytype__del_unique_historytype_namespace_n.py new file mode 100644 index 0000000000..0f86b89145 --- /dev/null +++ b/mayan/apps/history/south_migrations/0003_auto__del_history__del_historytype__del_unique_historytype_namespace_n.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + depends_on = ( + ('events', '0002_migrate_history_data'), + ) + + def forwards(self, orm): + # Removing unique constraint on 'HistoryType', fields ['namespace', 'name'] + db.delete_unique(u'history_historytype', ['namespace', 'name']) + + # Deleting model 'History' + db.delete_table(u'history_history') + + # Deleting model 'HistoryType' + db.delete_table(u'history_historytype') + + + def backwards(self, orm): + # Adding model 'History' + db.create_table(u'history_history', ( + ('history_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['history.HistoryType'])), + ('dictionary', self.gf('django.db.models.fields.TextField')(blank=True)), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), + ('datetime', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal(u'history', ['History']) + + # Adding model 'HistoryType' + db.create_table(u'history_historytype', ( + ('namespace', self.gf('django.db.models.fields.CharField')(max_length=64)), + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=64)), + )) + db.send_create_signal(u'history', ['HistoryType']) + + # Adding unique constraint on 'HistoryType', fields ['namespace', 'name'] + db.create_unique(u'history_historytype', ['namespace', 'name']) + + + models = { + + } + + complete_apps = ['history'] diff --git a/mayan/apps/history/urls.py b/mayan/apps/history/urls.py deleted file mode 100644 index 7dd85a29fb..0000000000 --- a/mayan/apps/history/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.conf.urls import patterns, url - -urlpatterns = patterns('history.views', - url(r'^list/$', 'history_list', (), 'history_list'), - url(r'^list/for_object/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', 'history_for_object', (), 'history_for_object'), - url(r'^(?P\d+)/$', 'history_view', (), 'history_view'), - url(r'^type/(?P\d+)/list/$', 'history_type_list', (), 'history_type_list'), -) diff --git a/mayan/apps/history/views.py b/mayan/apps/history/views.py deleted file mode 100644 index 12bdf7297f..0000000000 --- a/mayan/apps/history/views.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import absolute_import - -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied -from django.db.models.loading import get_model -from django.http import Http404 -from django.shortcuts import get_object_or_404, render_to_response -from django.template import RequestContext -from django.utils.translation import ugettext_lazy as _ - -from acls.models import AccessEntry -from common.utils import encapsulate -from permissions.models import Permission - -from .forms import HistoryDetailForm -from .models import History, HistoryType -from .permissions import PERMISSION_HISTORY_VIEW -from .widgets import history_entry_object_link - - -def history_list(request, object_list=None, title=None, extra_context=None): - pre_object_list = object_list if not (object_list is None) else History.objects.all() - - try: - Permission.objects.check_permissions(request.user, [PERMISSION_HISTORY_VIEW]) - except PermissionDenied: - # If user doesn't have global permission, get a list of document - # for which he/she does hace access use it to filter the - # provided object_list - final_object_list = AccessEntry.objects.filter_objects_by_access(PERMISSION_HISTORY_VIEW, request.user, pre_object_list, related='content_object') - else: - final_object_list = pre_object_list - - context = { - 'object_list': final_object_list, - 'title': title if title else _(u'History events'), - 'extra_columns': [ - { - 'name': _(u'Object link'), - 'attribute': encapsulate(lambda x: history_entry_object_link(x)) - }, - ], - 'hide_object': True, - } - - if extra_context: - context.update(extra_context) - - return render_to_response('main/generic_list.html', context, - context_instance=RequestContext(request)) - - -def history_for_object(request, app_label, module_name, object_id): - model = get_model(app_label, module_name) - if not model: - raise Http404 - content_object = get_object_or_404(model, pk=object_id) - content_type = ContentType.objects.get_for_model(model) - - try: - Permission.objects.check_permissions(request.user, [PERMISSION_HISTORY_VIEW]) - except PermissionDenied: - AccessEntry.objects.check_access(PERMISSION_HISTORY_VIEW, request.user, content_object) - - context = { - 'object_list': History.objects.filter(content_type=content_type, object_id=object_id), - 'title': _(u'History events for: %s') % content_object, - 'object': content_object, - 'hide_object': True, - } - - return render_to_response('main/generic_list.html', context, - context_instance=RequestContext(request)) - - -def history_view(request, object_id): - history = get_object_or_404(History, pk=object_id) - - try: - Permission.objects.check_permissions(request.user, [PERMISSION_HISTORY_VIEW]) - except PermissionDenied: - AccessEntry.objects.check_access(PERMISSION_HISTORY_VIEW, request.user, history.content_object) - - form = HistoryDetailForm(instance=history, extra_fields=[ - {'label': _(u'Date'), 'field': lambda x: x.datetime.date()}, - {'label': _(u'Time'), 'field': lambda x: unicode(x.datetime.time()).split('.')[0]}, - {'label': _(u'Object'), 'field': 'content_object'}, - {'label': _(u'Event type'), 'field': lambda x: x.get_label()}, - {'label': _(u'Additional details'), 'field': lambda x: x.get_processed_details() or _(u'None')}, - ]) - - return render_to_response('main/generic_detail.html', { - 'title': _(u'Details for: %s') % history.get_processed_summary(), - 'form': form, - }, context_instance=RequestContext(request)) - - -def history_type_list(request, history_type_pk): - history_type = get_object_or_404(HistoryType, pk=history_type_pk) - - return history_list( - request, - object_list=History.objects.filter(history_type=history_type), - title=_(u'History events of type: %s') % history_type, - ) diff --git a/mayan/apps/history/widgets.py b/mayan/apps/history/widgets.py deleted file mode 100644 index 8d4c9c160b..0000000000 --- a/mayan/apps/history/widgets.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.utils.html import escape -from django.utils.safestring import mark_safe - - -def history_entry_object_link(entry): - return mark_safe(u'%(label)s' % { - 'url': entry.content_object.get_absolute_url() if entry.content_object else u'#', - 'label': escape(unicode(entry.content_object)) if entry.content_object else u''} - ) - - -def history_entry_summary(entry): - return mark_safe(u'%(label)s' % { - 'url': entry.get_absolute_url(), - 'label': unicode(entry.get_processed_summary())} - ) - - -def history_entry_type_link(entry): - return mark_safe(u'%(label)s' % { - 'url': entry.history_type.get_absolute_url(), - 'label': unicode(entry.history_type)} - ) diff --git a/mayan/apps/user_management/__init__.py b/mayan/apps/user_management/__init__.py index 6a67c501e1..cc72c7007a 100644 --- a/mayan/apps/user_management/__init__.py +++ b/mayan/apps/user_management/__init__.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from django.contrib.auth.models import User, Group -from actstream import registry from navigation.api import register_links from navigation.links import link_spacer @@ -28,5 +27,4 @@ register_setup(group_setup) APIEndPoint('users', app_name='user_management') -registry.register(User) -registry.register(Group) + diff --git a/mayan/apps/user_management/models.py b/mayan/apps/user_management/models.py index 71a8362390..148f268626 100644 --- a/mayan/apps/user_management/models.py +++ b/mayan/apps/user_management/models.py @@ -1,3 +1,7 @@ from django.db import models +from django.contrib.auth.models import User, Group -# Create your models here. +from actstream import registry + +registry.register(User) +registry.register(Group) diff --git a/mayan/urls.py b/mayan/urls.py index 4f22ebaea1..a12b13e939 100644 --- a/mayan/urls.py +++ b/mayan/urls.py @@ -8,7 +8,6 @@ urlpatterns = patterns('', url(r'^', include('common.urls', namespace='common')), url(r'^', include('main.urls', namespace='main')), url(r'^acls/', include('acls.urls', namespace='acls')), - url(r'^activity/', include('actstream.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^api/', include('rest_api.urls')), url(r'^checkouts/', include('checkouts.urls', namespace='checkouts')), @@ -18,9 +17,9 @@ urlpatterns = patterns('', url(r'^documents/', include('documents.urls', namespace='documents')), url(r'^documents/signatures/', include('document_signatures.urls', namespace='signatures')), url(r'^docs/', include('rest_framework_swagger.urls')), + url(r'^events/', include('events.urls', namespace='events')), url(r'^folders/', include('folders.urls', namespace='folders')), url(r'^gpg/', include('django_gpg.urls', namespace='django_gpg')), - url(r'^history/', include('history.urls', namespace='history')), url(r'^installation/', include('installation.urls', namespace='installation')), url(r'^linking/', include('linking.urls', namespace='linking')), url(r'^mailer/', include('mailer.urls', namespace='mailer')),