Initial commit of the generic history app
This commit is contained in:
@@ -7,6 +7,7 @@ from navigation.api import register_links, register_menu, \
|
||||
from main.api import register_diagnostic, register_tool
|
||||
from permissions.api import register_permission, set_namespace_title
|
||||
from tags.widgets import get_tags_inline_widget_simple
|
||||
from history.api import register_history_type
|
||||
|
||||
from documents.models import Document, DocumentPage, DocumentPageTransformation
|
||||
from documents.staging import StagingFile
|
||||
@@ -17,7 +18,10 @@ from documents.literals import PERMISSION_DOCUMENT_CREATE, \
|
||||
PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, \
|
||||
PERMISSION_DOCUMENT_TRANSFORM, PERMISSION_DOCUMENT_TOOLS, \
|
||||
PERMISSION_DOCUMENT_EDIT
|
||||
from documents.literals import HISTORY_DOCUMENT_CREATED, \
|
||||
HISTORY_DOCUMENT_EDITED
|
||||
|
||||
# Permission setup
|
||||
set_namespace_title('documents', _(u'documents'))
|
||||
register_permission(PERMISSION_DOCUMENT_CREATE)
|
||||
register_permission(PERMISSION_DOCUMENT_PROPERTIES_EDIT)
|
||||
@@ -28,6 +32,10 @@ register_permission(PERMISSION_DOCUMENT_DOWNLOAD)
|
||||
register_permission(PERMISSION_DOCUMENT_TRANSFORM)
|
||||
register_permission(PERMISSION_DOCUMENT_TOOLS)
|
||||
|
||||
# History setup
|
||||
register_history_type(HISTORY_DOCUMENT_CREATED)
|
||||
register_history_type(HISTORY_DOCUMENT_EDITED)
|
||||
|
||||
document_list = {'text': _(u'documents list'), 'view': 'document_list', 'famfam': 'page', 'permissions': [PERMISSION_DOCUMENT_VIEW]}
|
||||
document_list_recent = {'text': _(u'recent documents list'), 'view': 'document_list_recent', 'famfam': 'page', 'permissions': [PERMISSION_DOCUMENT_VIEW]}
|
||||
document_create = {'text': _(u'upload a new document'), 'view': 'document_create', 'famfam': 'page_add', 'permissions': [PERMISSION_DOCUMENT_CREATE]}
|
||||
@@ -97,7 +105,6 @@ register_links(['document_page_view'], [document_page_rotate_left, document_page
|
||||
# Upload sources
|
||||
register_links(['upload_document_from_local', 'upload_document_from_staging', 'upload_document_from_user_staging'], [upload_document_from_local, upload_document_from_staging, upload_document_from_user_staging], menu_name='form_header')
|
||||
|
||||
|
||||
register_links(DocumentPageTransformation, [document_page_transformation_edit, document_page_transformation_delete])
|
||||
register_links(DocumentPageTransformation, [document_page_transformation_page_edit, document_page_transformation_page_view], menu_name='sidebar')
|
||||
register_links('document_page_transformation_list', [document_page_transformation_create], menu_name='sidebar')
|
||||
|
||||
@@ -17,3 +17,19 @@ PERMISSION_DOCUMENT_TOOLS = {'namespace': 'documents', 'name': 'document_tools',
|
||||
UPLOAD_SOURCE_LOCAL = u'local'
|
||||
UPLOAD_SOURCE_STAGING = u'staging'
|
||||
UPLOAD_SOURCE_USER_STAGING = u'user_staging'
|
||||
|
||||
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.get_full_name() if user.get_full_name() else user.username'}]
|
||||
}
|
||||
|
||||
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 edited on %(datetime)s by %(fullname)s.'),
|
||||
'expressions': [{'fullname': 'user.get_full_name() if user.get_full_name() else user.username'}]
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class Document(models.Model):
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_(u'description'), db_index=True)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
|
||||
comments = generic.GenericRelation(
|
||||
Comment,
|
||||
content_type_field='content_type',
|
||||
|
||||
@@ -35,6 +35,7 @@ from permissions.api import check_permissions
|
||||
from tags.utils import get_tags_subtemplate
|
||||
from document_indexing.utils import get_document_indexing_subtemplate
|
||||
from document_indexing.api import update_indexes, delete_indexes
|
||||
from history.api import create_history
|
||||
|
||||
from documents.conf.settings import DELETE_STAGING_FILE_AFTER_UPLOAD
|
||||
from documents.conf.settings import USE_STAGING_DIRECTORY
|
||||
@@ -57,6 +58,8 @@ from documents.literals import PERMISSION_DOCUMENT_CREATE, \
|
||||
PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD, \
|
||||
PERMISSION_DOCUMENT_TRANSFORM, \
|
||||
PERMISSION_DOCUMENT_EDIT
|
||||
from documents.literals import HISTORY_DOCUMENT_CREATED, \
|
||||
HISTORY_DOCUMENT_EDITED
|
||||
|
||||
from documents.forms import DocumentTypeSelectForm, \
|
||||
DocumentForm, DocumentForm_edit, DocumentPropertiesForm, \
|
||||
@@ -132,6 +135,8 @@ def _handle_save_document(request, document, form=None):
|
||||
for warning in warnings:
|
||||
messages.warning(request, warning)
|
||||
|
||||
create_history(HISTORY_DOCUMENT_CREATED, document, {'user': request.user})
|
||||
|
||||
|
||||
def _handle_zip_file(request, uploaded_file, document_type=None):
|
||||
filename = getattr(uploaded_file, 'filename', getattr(uploaded_file, 'name', ''))
|
||||
@@ -273,7 +278,7 @@ def document_view_simple(request, document_id):
|
||||
# Triggers a 404 error on documents uploaded via local upload
|
||||
# TODO: investigate
|
||||
document = get_object_or_404(Document, pk=document_id)
|
||||
|
||||
|
||||
RecentDocument.objects.add_document_for_user(request.user, document)
|
||||
|
||||
subtemplates_list = []
|
||||
@@ -491,8 +496,6 @@ def document_edit(request, document_id):
|
||||
|
||||
document = get_object_or_404(Document, pk=document_id)
|
||||
|
||||
RecentDocument.objects.add_document_for_user(request.user, document)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = DocumentForm_edit(request.POST, initial={'document_type': document.document_type})
|
||||
if form.is_valid():
|
||||
@@ -510,6 +513,9 @@ def document_edit(request, document_id):
|
||||
|
||||
document.save()
|
||||
|
||||
create_history(HISTORY_DOCUMENT_EDITED, document, {'user': request.user})
|
||||
RecentDocument.objects.add_document_for_user(request.user, document)
|
||||
|
||||
messages.success(request, _(u'Document %s edited successfully.') % document)
|
||||
|
||||
warnings = update_indexes(document)
|
||||
|
||||
19
apps/history/__init__.py
Normal file
19
apps/history/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from navigation.api import register_links, register_menu
|
||||
from permissions.api import register_permission, set_namespace_title
|
||||
|
||||
|
||||
PERMISSION_HISTORY_VIEW = {'namespace': 'history', 'name': u'history_view', 'label': _(u'Access the history app')}
|
||||
|
||||
set_namespace_title('history', _(u'history'))
|
||||
register_permission(PERMISSION_HISTORY_VIEW)
|
||||
|
||||
# TODO: support permissions AND operand
|
||||
history_list = {'text': _(u'history'), 'view': 'history_list', 'famfam': 'book', 'permissions': [PERMISSION_HISTORY_VIEW]}
|
||||
|
||||
register_links(['history_list'], [history_list], menu_name='sidebar')
|
||||
|
||||
register_menu([
|
||||
{'text': _(u'history'), 'view': 'history_list', 'links': [
|
||||
], 'famfam': 'book', 'position': 3}])
|
||||
57
apps/history/api.py
Normal file
57
apps/history/api.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import pickle
|
||||
import json
|
||||
|
||||
from django.db.utils import DatabaseError
|
||||
#from django.utils import simplejson
|
||||
from django.core import serializers
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db import models
|
||||
|
||||
from history.models import HistoryType, History
|
||||
from history.runtime_data import history_types_dict
|
||||
|
||||
|
||||
def register_history_type(history_type_dict):
|
||||
namespace = history_type_dict['namespace']
|
||||
name = history_type_dict['name']
|
||||
try:
|
||||
# Permanent
|
||||
history_type_obj, created = HistoryType.objects.get_or_create(
|
||||
namespace=namespace, name=name)
|
||||
history_type_obj.save()
|
||||
|
||||
# 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', []),
|
||||
}
|
||||
except DatabaseError:
|
||||
#Special case for ./manage.py syncdb
|
||||
pass
|
||||
|
||||
|
||||
def create_history(history_type_dict, source_object=None, data=None):
|
||||
history_type = get_object_or_404(HistoryType, 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()
|
||||
11
apps/history/forms.py
Normal file
11
apps/history/forms.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django import forms
|
||||
|
||||
from common.forms import DetailForm
|
||||
|
||||
from history.models import History
|
||||
|
||||
|
||||
class HistoryDetailForm(DetailForm):
|
||||
class Meta:
|
||||
model = History
|
||||
exclude = ('datetime', 'content_type', 'object_id', 'history_type', 'dictionary')
|
||||
10
apps/history/managers.py
Normal file
10
apps/history/managers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
#from django.shortcuts import get_object_or_404
|
||||
|
||||
|
||||
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])
|
||||
112
apps/history/models.py
Normal file
112
apps/history/models.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core import serializers
|
||||
|
||||
#from history.managers import HistoryManager
|
||||
from history.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 u'%s - %s' % (self.namespace, self.name)
|
||||
|
||||
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'))
|
||||
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)
|
||||
|
||||
#objects = HistoryManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s - %s - %s' % (self.datetime, self.content_object, self.history_type)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
self.datetime = datetime.now()
|
||||
super(History, self).save(*args, **kwargs)
|
||||
|
||||
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 expression_item in history.get_expressions():
|
||||
key, value = expression_item.items()[0]
|
||||
try:
|
||||
expressions_dict = {
|
||||
key: eval(value, key_values.copy())
|
||||
}
|
||||
except Exception, e:
|
||||
expressions_dict = {
|
||||
key: e
|
||||
}
|
||||
|
||||
key_values.update(expressions_dict)
|
||||
return text % key_values
|
||||
1
apps/history/runtime_data.py
Normal file
1
apps/history/runtime_data.py
Normal file
@@ -0,0 +1 @@
|
||||
history_types_dict = {}
|
||||
23
apps/history/tests.py
Normal file
23
apps/history/tests.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
This file demonstrates two different styles of tests (one doctest and one
|
||||
unittest). These will both pass when you run "manage.py test".
|
||||
|
||||
Replace these with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.failUnlessEqual(1 + 1, 2)
|
||||
|
||||
__test__ = {"doctest": """
|
||||
Another way to test that 1 + 1 is equal to 2.
|
||||
|
||||
>>> 1 + 1 == 2
|
||||
True
|
||||
"""}
|
||||
|
||||
7
apps/history/urls.py
Normal file
7
apps/history/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
urlpatterns = patterns('history.views',
|
||||
url(r'^list/$', 'history_list', (), 'history_list'),
|
||||
url(r'^list/for_object/(?P<content_type_id>\d+)/(?P<object_id>\d+)/$', 'history_for_object', (), 'history_for_object'),
|
||||
url(r'^(?P<object_id>\d+)/$', 'history_view', (), 'history_view'),
|
||||
)
|
||||
93
apps/history/views.py
Normal file
93
apps/history/views.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from permissions.api import check_permissions
|
||||
|
||||
from history.models import History
|
||||
from history.api import history_types_dict
|
||||
from history.forms import HistoryDetailForm
|
||||
from history import PERMISSION_HISTORY_VIEW
|
||||
|
||||
|
||||
def history_list(request):
|
||||
check_permissions(request.user, [PERMISSION_HISTORY_VIEW])
|
||||
|
||||
context = {
|
||||
'object_list': History.objects.all(),
|
||||
'title': _(u'history events'),
|
||||
'extra_columns': [
|
||||
{
|
||||
'name': _(u'date and time'),
|
||||
'attribute': 'datetime'
|
||||
},
|
||||
{
|
||||
'name': _(u'summary'),
|
||||
'attribute': lambda x: '<a href="%(url)s">%(label)s</a>' % {
|
||||
'url': x.get_absolute_url(),
|
||||
'label': unicode(x.get_processed_summary())
|
||||
}
|
||||
}
|
||||
],
|
||||
'hide_object': True,
|
||||
}
|
||||
|
||||
return render_to_response('generic_list.html', context,
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def history_for_object(request, content_type_id, object_id):
|
||||
check_permissions(request.user, [PERMISSION_HISTORY_VIEW])
|
||||
|
||||
content_type = get_object_or_404(ContentType, pk=content_type_id)
|
||||
content_object = get_object_or_404(content_type.model_class(), pk=object_id)
|
||||
|
||||
context = {
|
||||
'object_list': History.objects.filter(content_type=content_type_id, object_id=object_id),
|
||||
'title': _(u'history for: %s') % content_object,
|
||||
'extra_columns': [
|
||||
{
|
||||
'name': _(u'date and time'),
|
||||
'attribute': 'datetime'
|
||||
},
|
||||
{
|
||||
'name': _(u'summary'),
|
||||
'attribute': lambda x: '<a href="%(url)s">%(label)s</a>' % {
|
||||
'url': x.get_absolute_url(),
|
||||
'label': unicode(x.get_processed_summary())
|
||||
}
|
||||
}
|
||||
],
|
||||
'hide_object': True,
|
||||
}
|
||||
|
||||
return render_to_response('generic_list.html', context,
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def history_view(request, object_id):
|
||||
check_permissions(request.user, [PERMISSION_HISTORY_VIEW])
|
||||
|
||||
history = get_object_or_404(History, pk=object_id)
|
||||
|
||||
|
||||
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'Event details'), 'field': lambda x: x.get_processed_details()},
|
||||
])
|
||||
|
||||
return render_to_response('generic_detail.html', {
|
||||
'title': _(u'details for: %s') % history.get_processed_summary(),
|
||||
'form': form,
|
||||
'object': history,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
@@ -122,6 +122,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.comments',
|
||||
'smart_settings',
|
||||
'navigation',
|
||||
'history',
|
||||
'web_theme',
|
||||
'main',
|
||||
'common',
|
||||
|
||||
Reference in New Issue
Block a user