diff --git a/mayan/apps/checkouts/__init__.py b/mayan/apps/checkouts/__init__.py index 6b556e3bf3..3b2fcad171 100644 --- a/mayan/apps/checkouts/__init__.py +++ b/mayan/apps/checkouts/__init__.py @@ -5,14 +5,9 @@ from datetime import timedelta from acls.api import class_permissions from documents.models import Document from mayan.celery import app -from history.api import register_history_type from navigation.api import register_links, register_top_menu from rest_api.classes import APIEndPoint -from .events import (HISTORY_DOCUMENT_AUTO_CHECKED_IN, - HISTORY_DOCUMENT_CHECKED_OUT, - HISTORY_DOCUMENT_CHECKED_IN, - HISTORY_DOCUMENT_FORCEFUL_CHECK_IN) from .links import (checkin_document, checkout_document, checkout_info, checkout_list) from .models import DocumentCheckout @@ -48,10 +43,6 @@ class_permissions(Document, [ ]) initialize_document_checkout_extra_methods() -register_history_type(HISTORY_DOCUMENT_CHECKED_OUT) -register_history_type(HISTORY_DOCUMENT_CHECKED_IN) -register_history_type(HISTORY_DOCUMENT_AUTO_CHECKED_IN) -register_history_type(HISTORY_DOCUMENT_FORCEFUL_CHECK_IN) register_links(Document, [checkout_info], menu_name='form_header') register_links(['checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:checkin_document'], [checkout_document, checkin_document], menu_name="sidebar") diff --git a/mayan/apps/checkouts/events.py b/mayan/apps/checkouts/events.py index a35503a819..747da656df 100644 --- a/mayan/apps/checkouts/events.py +++ b/mayan/apps/checkouts/events.py @@ -1,5 +1,11 @@ +from __future__ import absolute_import + 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'), @@ -7,6 +13,8 @@ HISTORY_DOCUMENT_CHECKED_OUT = { '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'), @@ -14,12 +22,16 @@ HISTORY_DOCUMENT_CHECKED_IN = { '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'), diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index c18d5f1643..31ccf449de 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -8,12 +8,11 @@ from django.utils.timezone import now from acls.models import AccessEntry from documents.models import Document -from history.api import create_history from permissions.models import Permission -from .events import (HISTORY_DOCUMENT_AUTO_CHECKED_IN, - HISTORY_DOCUMENT_CHECKED_IN, - HISTORY_DOCUMENT_FORCEFUL_CHECK_IN) +from .events import (event_document_auto_check_in, + event_document_check_in, + event_document_forceful_check_in) from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN from .permissions import PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE @@ -48,11 +47,11 @@ class DocumentCheckoutManager(models.Manager): else: if user: if self.document_checkout_info(document).user_object != user: - create_history(HISTORY_DOCUMENT_FORCEFUL_CHECK_IN, source_object=document, data={'user': user, 'document': document}) + event_document_forceful_check_in.commit(actor=user, target=document) else: - create_history(HISTORY_DOCUMENT_CHECKED_IN, source_object=document, data={'user': user, 'document': document}) + event_document_check_in.commit(actor=user, target=document) else: - create_history(HISTORY_DOCUMENT_AUTO_CHECKED_IN, source_object=document, data={'document': document}) + event_document_auto_check_in.commit(target=document) document_checkout.delete() diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index 9d572c21f8..b85311c991 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -8,9 +8,8 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from documents.models import Document -from history.api import create_history -from .events import HISTORY_DOCUMENT_CHECKED_OUT +from .events import event_document_check_out from .exceptions import DocumentAlreadyCheckedOut from .managers import DocumentCheckoutManager @@ -48,7 +47,7 @@ class DocumentCheckout(models.Model): result = super(DocumentCheckout, self).save(*args, **kwargs) if new_checkout: - create_history(HISTORY_DOCUMENT_CHECKED_OUT, source_object=self.document, data={'user': self.user_object, 'document': self.document}) + event_document_check_out.commit(actor=self.user_object, target=self.document) return result @models.permalink diff --git a/mayan/apps/documents/__init__.py b/mayan/apps/documents/__init__.py index 56976a89e4..16f968e6a3 100644 --- a/mayan/apps/documents/__init__.py +++ b/mayan/apps/documents/__init__.py @@ -8,7 +8,6 @@ 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.api import register_history_type from history.permissions import PERMISSION_HISTORY_VIEW from main.api import register_maintenance_links from navigation.api import (register_links, register_model_list_columns) @@ -18,8 +17,6 @@ from rest_api.classes import APIEndPoint from statistics.classes import StatisticNamespace from documents import settings as document_settings -from .events import (HISTORY_DOCUMENT_CREATED, - HISTORY_DOCUMENT_DELETED, HISTORY_DOCUMENT_EDITED) from .links import (document_clear_image_cache, document_clear_transformations, document_content, document_delete, @@ -63,11 +60,6 @@ from .settings import THUMBNAIL_SIZE from .statistics import DocumentStatistics, DocumentUsageStatistics from .widgets import document_thumbnail -# History setup -register_history_type(HISTORY_DOCUMENT_CREATED) -register_history_type(HISTORY_DOCUMENT_EDITED) -register_history_type(HISTORY_DOCUMENT_DELETED) - # Register document type links register_links(DocumentType, [document_type_edit, document_type_filename_list, document_type_delete]) register_links([DocumentType, 'documents:document_type_create', 'documents:document_type_list'], [document_type_list, document_type_create], menu_name='secondary_menu') diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 6f8ed92e7f..1e64964af0 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -1,5 +1,11 @@ +from __future__ import absolute_import + 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'), @@ -8,6 +14,8 @@ HISTORY_DOCUMENT_CREATED = { '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'), @@ -17,11 +25,3 @@ HISTORY_DOCUMENT_EDITED = { 'fullname': 'user[0]["fields"]["username"] if isinstance(user, list) else user.get_full_name() if user.get_full_name() else user.username', } } - -HISTORY_DOCUMENT_DELETED = { - 'namespace': 'documents', 'name': 'document_deleted', - 'label': _(u'Document deleted'), - 'summary': _(u'Document "%(document)s" deleted by %(fullname)s.'), - 'details': _(u'Document "%(document)s" deleted on %(datetime)s by %(fullname)s.'), - 'expressions': {'fullname': 'user.get_full_name() if user.get_full_name() else user.username'} -} diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index c2a817f36f..9eb0c7ae1e 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -19,6 +19,8 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ +from actstream import registry + from acls.utils import apply_default_acls from common.settings import TEMPORARY_DIRECTORY from converter.api import (convert, get_page_count, @@ -26,10 +28,9 @@ from converter.api import (convert, get_page_count, from converter.exceptions import UnknownFileFormat from converter.literals import (DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, DEFAULT_PAGE_NUMBER) -from history.api import create_history from mimetype.api import get_mimetype -from .events import HISTORY_DOCUMENT_CREATED +from .events import event_document_create from .exceptions import NewDocumentVersionNotAllowed from .literals import LANGUAGE_CHOICES from .managers import (DocumentManager, DocumentPageTransformationManager, @@ -119,9 +120,9 @@ class Document(models.Model): if user: self.add_as_recent_document_for_user(user) - create_history(HISTORY_DOCUMENT_CREATED, self, {'user': user}) + event_document_create.commit(actor=user, target=self) else: - create_history(HISTORY_DOCUMENT_CREATED, self) + event_document_create.commit(target=self) def get_cached_image_name(self, page, version): document_version = DocumentVersion.objects.get(pk=version) @@ -582,3 +583,5 @@ class RecentDocument(models.Model): # Quick hack to break the DocumentPage and DocumentPageTransformation circular dependency # Can be remove once the transformations are moved to the converter app DocumentPage.add_to_class('get_transformation_list', lambda document_page: DocumentPageTransformation.objects.get_for_document_page_as_list(document_page)) + +registry.register(Document) diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index ac20aa410e..883d9c2c3f 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -23,11 +23,10 @@ from common.widgets import two_state_template from converter.literals import (DEFAULT_FILE_FORMAT_MIMETYPE, DEFAULT_PAGE_NUMBER, DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL) from filetransfers.api import serve_file -from history.api import create_history from navigation.utils import resolve_to_name from permissions.models import Permission -from .events import HISTORY_DOCUMENT_EDITED +from .events import event_document_edited from .forms import (DocumentContentForm, DocumentDownloadForm, DocumentForm, DocumentPageForm, DocumentPageForm_edit, DocumentPageForm_text, DocumentPageTransformationForm, @@ -238,7 +237,7 @@ def document_edit(request, document_id): document.label = form.cleaned_data['document_type_available_filenames'].filename document.save() - create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user}) + event_document_edited.commit(actor=request.user) document.add_as_recent_document_for_user(request.user) messages.success(request, _(u'Document "%s" edited successfully.') % document) @@ -280,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']) - create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user}) + event_document_edited.commit(actor=request.user) 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 new file mode 100644 index 0000000000..6a821ede7a --- /dev/null +++ b/mayan/apps/events/__init__.py @@ -0,0 +1 @@ +from .classes import Event # NOQA diff --git a/mayan/apps/events/admin.py b/mayan/apps/events/admin.py new file mode 100644 index 0000000000..f22496a166 --- /dev/null +++ b/mayan/apps/events/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from .models import EventType + +class EventTypeAdmin(admin.ModelAdmin): + readonly_fields = ('name', 'get_label') + + +admin.site.register(EventType, EventTypeAdmin) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py new file mode 100644 index 0000000000..222dc59558 --- /dev/null +++ b/mayan/apps/events/classes.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + +from django.core import serializers +from django.db import models + +from actstream import action + + +class Event(object): + def __init__(self, name, label): + self.name = name + self.label = label + self.event_type = None + + 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) + + action.send(actor or target, actor=actor, verb=self.name, action_object=action_object, target=target) diff --git a/mayan/apps/events/managers.py b/mayan/apps/events/managers.py new file mode 100644 index 0000000000..be2ed66f25 --- /dev/null +++ b/mayan/apps/events/managers.py @@ -0,0 +1,23 @@ +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/migrations/0001_initial.py b/mayan/apps/events/migrations/0001_initial.py new file mode 100644 index 0000000000..01594af23e --- /dev/null +++ b/mayan/apps/events/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- 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): + + def forwards(self, orm): + # Adding model 'EventType' + db.create_table(u'events_eventtype', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=64)), + )) + db.send_create_signal(u'events', ['EventType']) + + + def backwards(self, orm): + # Deleting model 'EventType' + db.delete_table(u'events_eventtype') + + + models = { + 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'}) + } + } + + complete_apps = ['events'] \ No newline at end of file diff --git a/mayan/apps/events/migrations/__init__.py b/mayan/apps/events/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/events/models.py b/mayan/apps/events/models.py new file mode 100644 index 0000000000..57192c5d86 --- /dev/null +++ b/mayan/apps/events/models.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals + +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 + + +@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]) + + class Meta: + verbose_name = _('Event type') + verbose_name_plural = _('Event types') diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/mayan/apps/events/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/mayan/apps/history/api.py b/mayan/apps/history/api.py deleted file mode 100644 index 7951ee0d33..0000000000 --- a/mayan/apps/history/api.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import absolute_import - -import json -import pickle - -from django.db import models -from django.core import serializers - -from .models import HistoryType, History -from .runtime_data import history_types_dict - - -def register_history_type(history_type_dict): - namespace = history_type_dict['namespace'] - name = history_type_dict['name'] - - # Runtime - history_types_dict.setdefault(namespace, {}) - history_types_dict[namespace][name] = { - 'label': history_type_dict['label'], - 'summary': history_type_dict.get('summary', u''), - 'details': history_type_dict.get('details', u''), - 'expressions': history_type_dict.get('expressions', {}), - } - - -def create_history(history_type_dict, source_object=None, data=None): - history_type, created = HistoryType.objects.get_or_create(namespace=history_type_dict['namespace'], name=history_type_dict['name']) - - new_history = History(history_type=history_type) - if source_object: - new_history.content_object = source_object - if data: - new_dict = {} - for key, value in data.items(): - new_dict[key] = {} - if isinstance(value, models.Model): - new_dict[key]['value'] = serializers.serialize('json', [value]) - elif isinstance(value, models.query.QuerySet): - new_dict[key]['value'] = serializers.serialize('json', value) - else: - new_dict[key]['value'] = json.dumps(value) - new_dict[key]['type'] = pickle.dumps(type(value)) - - new_history.dictionary = json.dumps(new_dict) - new_history.save() diff --git a/mayan/apps/history/models.py b/mayan/apps/history/models.py index 9c49deab81..8dda724f39 100644 --- a/mayan/apps/history/models.py +++ b/mayan/apps/history/models.py @@ -10,7 +10,7 @@ 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 +#from .runtime_data import history_types_dict class HistoryType(models.Model): @@ -18,10 +18,11 @@ class HistoryType(models.Model): name = models.CharField(max_length=64, verbose_name=_(u'Name')) def __unicode__(self): - try: - return unicode(history_types_dict[self.namespace][self.name]['label']) - except KeyError: - return u'Obsolete history type: %s - %s' % (self.namespace, self.name) + 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]) diff --git a/mayan/apps/history/runtime_data.py b/mayan/apps/history/runtime_data.py deleted file mode 100644 index a8acc92e5d..0000000000 --- a/mayan/apps/history/runtime_data.py +++ /dev/null @@ -1 +0,0 @@ -history_types_dict = {} diff --git a/mayan/apps/user_management/__init__.py b/mayan/apps/user_management/__init__.py index f79a2d29f9..6a67c501e1 100644 --- a/mayan/apps/user_management/__init__.py +++ b/mayan/apps/user_management/__init__.py @@ -2,6 +2,8 @@ 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 from project_setup.api import register_setup @@ -25,3 +27,6 @@ register_setup(user_setup) register_setup(group_setup) APIEndPoint('users', app_name='user_management') + +registry.register(User) +registry.register(Group) diff --git a/mayan/settings/base.py b/mayan/settings/base.py index 90befe691d..82a1cb782c 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -34,7 +34,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( - # Mayan EDMS + # 3rd party 'suit', # Django 'django.contrib.admin', @@ -78,6 +78,7 @@ INSTALLED_APPS = ( 'document_indexing', 'document_signatures', 'documents', + 'events', 'folders', 'history', 'installation', @@ -94,6 +95,9 @@ INSTALLED_APPS = ( 'tags', # Placed after rest_api to allow template overriding 'rest_framework_swagger', + # Must be last on Django < 1.7 as per documentation + # https://django-activity-stream.readthedocs.org/en/latest/installation.html + 'actstream', ) MIDDLEWARE_CLASSES = ( diff --git a/mayan/urls.py b/mayan/urls.py index 729c6e46e4..4f22ebaea1 100644 --- a/mayan/urls.py +++ b/mayan/urls.py @@ -8,6 +8,7 @@ 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')), diff --git a/requirements/common.txt b/requirements/common.txt index e6a0feced0..8d141f5139 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -2,6 +2,7 @@ celery==3.1.17 cssmin==0.2.0 Django==1.6.8 +django-activity-stream==0.5.1 django-celery==3.1.16 django-compressor==1.4 django-cors-headers==0.13