Initial commit of the generic history app

This commit is contained in:
Roberto Rosario
2011-05-26 05:07:26 -04:00
parent ecdc72f16c
commit 6c3aa1f37b
15 changed files with 369 additions and 5 deletions

View File

@@ -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')

View File

@@ -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'}]
}

View File

@@ -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',

View File

@@ -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
View 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
View 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
View 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
View 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
View 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

View File

@@ -0,0 +1 @@
history_types_dict = {}

23
apps/history/tests.py Normal file
View 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
View 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
View 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))

View File

@@ -122,6 +122,7 @@ INSTALLED_APPS = (
'django.contrib.comments',
'smart_settings',
'navigation',
'history',
'web_theme',
'main',
'common',

View File

@@ -23,6 +23,7 @@ urlpatterns = patterns('',
(r'^metadata/', include('metadata.urls')),
(r'^grouping/', include('grouping.urls')),
(r'^document_indexing/', include('document_indexing.urls')),
(r'^history/', include('history.urls')),
)