diff --git a/README.md b/README.md index 70691cac63..e91aa08fa8 100755 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Features * Previews for a great deal of image formats, including PDF * Document OCR and searching * Group documents by metadata automatically +* Permissions and roles support Requirements --- diff --git a/apps/common/api.py b/apps/common/api.py index da1af95e13..78df9d5bb4 100755 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -2,9 +2,6 @@ import copy from django.db.utils import DatabaseError -from permissions.utils import has_permission -from permissions.models import Permission - object_navigation = {} menu_links = [] @@ -44,25 +41,3 @@ def register_model_list_columns(model, columns): model_list_columns[model].extend(columns) else: model_list_columns[model] = copy.copy(columns) - - -def register_permissions(app, permissions): - if permissions: - for permission in permissions: - full_permission_name = "%s_%s" % (app, permission['name']) - try: - #if not Permission.objects.filter(codename=full_permission_name): - # Permission(name=unicode(permission['label']), codename=full_permission_name).save() - permission_obj, created = Permission.objects.get_or_create(codename=full_permission_name) - permission_obj.name=unicode(permission['label']) - permission_obj.save() - except DatabaseError: - #Special case for ./manage.py syncdb - pass - - -def check_permissions(object, user, permission_list): - temp_role = [] - for permission in permission_list: - if has_permission(object, user, permission): - return True diff --git a/apps/common/templates/generic_navigation.html b/apps/common/templates/generic_navigation.html index b7c86dd7cf..b18bd8a1d9 100755 --- a/apps/common/templates/generic_navigation.html +++ b/apps/common/templates/generic_navigation.html @@ -1,7 +1,17 @@ {% load i18n %} +{% load permission_tags %} {% for link in object_navigation_links %} +{% if link.permissions %} + {% check_permissions request.user link.permissions.namespace link.permissions.permissions %} + {% if permission %} + {% if as_li %}
  • {% endif %} + {% if link.famfam %}{% endif %}{{ link.text|capfirst }}{% if link.error %} - {{ link.error }}{% endif %}{% if link.active %}{% endif %}{% if horizontal %}{% if not forloop.last %} | {% endif %}{% endif %} + {% if as_li %}
  • {% endif %} + {% endif %} +{% else %} {% if as_li %}
  • {% endif %} {% if link.famfam %}{% endif %}{{ link.text|capfirst }}{% if link.error %} - {{ link.error }}{% endif %}{% if link.active %}{% endif %}{% if horizontal %}{% if not forloop.last %} | {% endif %}{% endif %} {% if as_li %}
  • {% endif %} +{% endif %} {% endfor %} diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py index 1e36969cb7..7f616f1cf6 100755 --- a/apps/documents/__init__.py +++ b/apps/documents/__init__.py @@ -4,25 +4,42 @@ from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from common.api import register_links, register_menu, \ - register_model_list_columns, register_permissions + register_model_list_columns from common.utils import pretty_size +from permissions.api import register_permissions + from models import Document from staging import StagingFile from documents.conf import settings as documents_settings +PERMISSION_DOCUMENT_CREATE = 'document_create' +PERMISSION_DOCUMENT_PROPERTIES_EDIT = 'document_properties_edit' +PERMISSION_DOCUMENT_METADATA_EDIT = 'document_metadata_edit' +PERMISSION_DOCUMENT_VIEW = 'document_view' +PERMISSION_DOCUMENT_DELETE = 'document_delete' +PERMISSION_DOCUMENT_DOWNLOAD = 'document_download' -document_list = {'text':_(u'documents list'), 'view':'document_list', 'famfam':'page'} -document_create = {'text':_('upload a document'), 'view':'document_create', 'famfam':'page_add'} -document_create_multiple = {'text':_('upload multiple documents'), 'view':'document_create_multiple', 'famfam':'page_add'} -document_create_sibling = {'text':_('upload using same metadata'), 'view':'document_create_sibling', 'args':'object.id', 'famfam':'page_copy'} -document_view = {'text':_('details'), 'view':'document_view', 'args':'object.id', 'famfam':'page'} -document_delete = {'text':_('delete'), 'view':'document_delete', 'args':'object.id', 'famfam':'page_delete'} -document_edit = {'text':_('edit'), 'view':'document_edit', 'args':'object.id', 'famfam':'page_edit'} -document_edit_metadata = {'text':_('edit metadata'), 'view':'document_edit_metadata', 'args':'object.id', 'famfam':'page_edit'} -document_preview = {'text':_('preview'), 'class':'fancybox', 'view':'document_preview', 'args':'object.id', 'famfam':'magnifier'} -document_download = {'text':_('download'), 'view':'document_download', 'args':'object.id', 'famfam':'page_save'} +register_permissions('documents', [ + {'name':PERMISSION_DOCUMENT_CREATE, 'label':_(u'Create document')}, + {'name':PERMISSION_DOCUMENT_PROPERTIES_EDIT, 'label':_(u'Edit document properties')}, + {'name':PERMISSION_DOCUMENT_METADATA_EDIT, 'label':_(u'Edit document metadata')}, + {'name':PERMISSION_DOCUMENT_VIEW, 'label':_(u'View document')}, + {'name':PERMISSION_DOCUMENT_DELETE, 'label':_(u'Delete document')}, + {'name':PERMISSION_DOCUMENT_DOWNLOAD, 'label':_(u'Download document')}, +]) + +document_list = {'text':_(u'documents list'), 'view':'document_list', 'famfam':'page', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_VIEW]}} +document_create = {'text':_('upload a document'), 'view':'document_create', 'famfam':'page_add', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_CREATE]}} +document_create_multiple = {'text':_('upload multiple documents'), 'view':'document_create_multiple', 'famfam':'page_add', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_CREATE]}} +document_create_sibling = {'text':_('upload using same metadata'), 'view':'document_create_sibling', 'args':'object.id', 'famfam':'page_copy', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_CREATE]}} +document_view = {'text':_('details'), 'view':'document_view', 'args':'object.id', 'famfam':'page', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_VIEW]}} +document_delete = {'text':_('delete'), 'view':'document_delete', 'args':'object.id', 'famfam':'page_delete', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_DELETE]}} +document_edit = {'text':_('edit'), 'view':'document_edit', 'args':'object.id', 'famfam':'page_edit', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_PROPERTIES_EDIT]}} +document_edit_metadata = {'text':_('edit metadata'), 'view':'document_edit_metadata', 'args':'object.id', 'famfam':'page_edit', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_METADATA_EDIT]}} +document_preview = {'text':_('preview'), 'class':'fancybox', 'view':'document_preview', 'args':'object.id', 'famfam':'magnifier', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_VIEW]}} +document_download = {'text':_('download'), 'view':'document_download', 'args':'object.id', 'famfam':'page_save', 'permissions':{'namespace':'documents', 'permissions':[PERMISSION_DOCUMENT_DOWNLOAD]}} staging_file_preview = {'text':_('preview'), 'class':'fancybox', 'view':'staging_file_preview', 'args':'object.id', 'famfam':'drive_magnify'} staging_file_delete = {'text':_('delete'), 'view':'staging_file_delete', 'args':'object.id', 'famfam':'drive_delete'} @@ -50,21 +67,3 @@ register_menu([ ],'famfam':'page','position':4}]) TEMPORARY_DIRECTORY = documents_settings.TEMPORARY_DIRECTORY if documents_settings.TEMPORARY_DIRECTORY else tempfile.mkdtemp() - -PERMISSION_DOCUMENT_CREATE = 'document_create' -PERMISSION_DOCUMENT_PROPERTIES_EDIT = 'document_properties_edit' -PERMISSION_DOCUMENT_METADATA_EDIT = 'document_metadata_edit' -PERMISSION_DOCUMENT_VIEW = 'document_view' -PERMISSION_DOCUMENT_DELETE = 'document_delete' -PERMISSION_DOCUMENT_OCR = 'document_ocr' -PERMISSION_DOCUMENT_DOWNLOAD = 'document_download' - -register_permissions('documents', [ - {'name':PERMISSION_DOCUMENT_CREATE, 'label':_(u'Create document')}, - {'name':PERMISSION_DOCUMENT_PROPERTIES_EDIT, 'label':_(u'Edit document properties')}, - {'name':PERMISSION_DOCUMENT_METADATA_EDIT, 'label':_(u'Edit document metadata')}, - {'name':PERMISSION_DOCUMENT_VIEW, 'label':_(u'View document')}, - {'name':PERMISSION_DOCUMENT_DELETE, 'label':_(u'Delete document')}, - {'name':PERMISSION_DOCUMENT_OCR, 'label':_(u'Submit document for OCR')}, - {'name':PERMISSION_DOCUMENT_DOWNLOAD, 'label':_(u'Download document')}, -]) diff --git a/apps/documents/views.py b/apps/documents/views.py index ea853cfbca..065edbcfa0 100755 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -11,7 +11,7 @@ from django.conf import settings from django.utils.http import urlencode from django.template.defaultfilters import slugify - +from permissions.api import check_permissions, Unauthorized from filetransfers.api import serve_file from converter.api import convert, in_image_cache, QUALITY_DEFAULT from common.utils import pretty_size @@ -38,13 +38,17 @@ from documents.conf.settings import GROUP_SHOW_EMPTY from documents import PERMISSION_DOCUMENT_CREATE, \ PERMISSION_DOCUMENT_CREATE, PERMISSION_DOCUMENT_PROPERTIES_EDIT, \ PERMISSION_DOCUMENT_METADATA_EDIT, PERMISSION_DOCUMENT_VIEW, \ - PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_OCR, \ - PERMISSION_DOCUMENT_DOWNLOAD + PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_DOWNLOAD - from utils import save_metadata, save_metadata_list, decode_metadata_from_url def document_list(request): + permissions = [PERMISSION_DOCUMENT_VIEW] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + return object_list( request, queryset=Document.objects.all(), @@ -56,8 +60,10 @@ def document_list(request): def document_create(request, multiple=True): permissions = [PERMISSION_DOCUMENT_CREATE] - if not check_permissions(main_object, request.user, permissions): - raise Http404 + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) if DocumentType.objects.all().count() == 1: wizard = DocumentCreateWizard( @@ -72,6 +78,12 @@ def document_create(request, multiple=True): return wizard(request) def document_create_sibling(request, document_id, multiple=True): + permissions = [PERMISSION_DOCUMENT_CREATE] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) urldata = [] for id, metadata in enumerate(document.documentmetadata_set.all()): @@ -89,6 +101,12 @@ def document_create_sibling(request, document_id, multiple=True): def upload_document_with_type(request, document_type_id, multiple=True): + permissions = [PERMISSION_DOCUMENT_CREATE] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document_type = get_object_or_404(DocumentType, pk=document_type_id) local_form = DocumentForm(prefix='local', initial={'document_type':document_type}) if USE_STAGING_DIRECTORY: @@ -206,6 +224,12 @@ def upload_document_with_type(request, document_type_id, multiple=True): context_instance=RequestContext(request)) def document_view(request, document_id): + permissions = [PERMISSION_DOCUMENT_VIEW] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) form = DocumentForm_view(instance=document, extra_fields=[ {'label':_(u'Filename'), 'field':'file_filename'}, @@ -285,6 +309,12 @@ def document_view(request, document_id): def document_delete(request, document_id): + permissions = [PERMISSION_DOCUMENT_DELETE] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) return delete_object(request, model=Document, object_id=document_id, @@ -298,6 +328,12 @@ def document_delete(request, document_id): def document_edit(request, document_id): + permissions = [PERMISSION_DOCUMENT_PROPERTIES_EDIT] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) if request.method == 'POST': form = DocumentForm_edit(request.POST, initial={'document_type':document.document_type}) @@ -338,6 +374,12 @@ def document_edit(request, document_id): def document_edit_metadata(request, document_id): + permissions = [PERMISSION_DOCUMENT_METADATA_EDIT] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) initial=[] @@ -386,6 +428,12 @@ def document_edit_metadata(request, document_id): def get_document_image(request, document_id, size=PREVIEW_SIZE, quality=QUALITY_DEFAULT): + permissions = [PERMISSION_DOCUMENT_VIEW] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) try: @@ -407,6 +455,12 @@ def get_document_image(request, document_id, size=PREVIEW_SIZE, quality=QUALITY_ def document_download(request, document_id): + permissions = [PERMISSION_DOCUMENT_DOWNLOAD] + try: + check_permissions(request.user, 'documents', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) try: #Test permissions and trigger exception @@ -417,6 +471,7 @@ def document_download(request, document_id): return HttpResponseRedirect(request.META['HTTP_REFERER']) +#TODO: Need permission def staging_file_preview(request, staging_file_id): try: filepath = StagingFile.get(staging_file_id).filepath @@ -425,7 +480,8 @@ def staging_file_preview(request, staging_file_id): except Exception, e: return serve_file(request, File(file=open('%simages/1297211435_error.png' % settings.MEDIA_ROOT, 'r'))) - + +#TODO: Need permission def staging_file_delete(request, staging_file_id): staging_file = StagingFile.get(staging_file_id) next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', None))) diff --git a/apps/ocr/__init__.py b/apps/ocr/__init__.py index 341df0c0ac..44466ab757 100755 --- a/apps/ocr/__init__.py +++ b/apps/ocr/__init__.py @@ -1,11 +1,17 @@ from django.utils.translation import ugettext_lazy as _ from common.api import register_links, register_menu +from permissions.api import register_permissions from documents.models import Document +OCR_DOCUMENT_OCR = 'document_ocr' -submit_document = {'text':_('submit to OCR queue'), 'view':'submit_document', 'args':'object.id', 'famfam':'page_lightning'} +register_permissions('ocr', [ + {'name':OCR_DOCUMENT_OCR, 'label':_(u'Submit document for OCR')}, +]) + +submit_document = {'text':_('submit to OCR queue'), 'view':'submit_document', 'args':'object.id', 'famfam':'page_lightning', 'permissions':{'namespace':'ocr', 'permissions':[OCR_DOCUMENT_OCR]}} register_links(Document, [submit_document], menu_name='sidebar') diff --git a/apps/ocr/views.py b/apps/ocr/views.py index 19ae631664..f3a7fe3def 100755 --- a/apps/ocr/views.py +++ b/apps/ocr/views.py @@ -1,4 +1,4 @@ -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import render_to_response, get_object_or_404, redirect from django.template import RequestContext from django.contrib import messages @@ -8,13 +8,19 @@ from django.core.urlresolvers import reverse from django.conf import settings from django.utils.translation import ugettext as _ - +from permissions.api import check_permissions, Unauthorized from documents.models import Document - +from ocr import OCR_DOCUMENT_OCR from api import ocr_document def submit_document(request, document_id): + permissions = [OCR_DOCUMENT_OCR] + try: + check_permissions(request.user, 'ocr', permissions) + except Unauthorized, e: + raise Http404(e) + document = get_object_or_404(Document, pk=document_id) try: diff --git a/apps/permissions/__init__.py b/apps/permissions/__init__.py new file mode 100644 index 0000000000..b2c3b5f6d6 --- /dev/null +++ b/apps/permissions/__init__.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.core.exceptions import ObjectDoesNotExist + +from permissions.conf.settings import DEFAULT_ROLES + +from models import Role + + +def user_post_save(sender, instance, **kwargs): + for default_role in DEFAULT_ROLES: + if isinstance(default_role, Role): + default_role.add_member(instance) + else: + try: + role = Role.objects.get(name=default_role) + role.add_member(instance) + except ObjectDoesNotExist: + pass + +post_save.connect(user_post_save, sender=User) diff --git a/apps/permissions/admin.py b/apps/permissions/admin.py new file mode 100644 index 0000000000..96ae45f5e5 --- /dev/null +++ b/apps/permissions/admin.py @@ -0,0 +1,33 @@ +from django.contrib import admin + +from models import Permission, PermissionHolder, Role, RoleMember +from django.contrib.contenttypes import generic + + +class PermissionHolderInline(admin.StackedInline): + model = PermissionHolder + extra = 1 + classes = ('collapse-open',) + allow_add = True + + +class PermissionAdmin(admin.ModelAdmin): + inlines = [PermissionHolderInline] + list_display = ('namespace', 'name', 'label') + list_display_links = list_display + + +class RoleMemberInline(admin.StackedInline): + model = RoleMember + extra = 1 + classes = ('collapse-open',) + allow_add = True + + +class RoleAdmin(admin.ModelAdmin): + inlines = [RoleMemberInline] + + +admin.site.register(Permission, PermissionAdmin) +admin.site.register(Role, RoleAdmin) + diff --git a/apps/permissions/api.py b/apps/permissions/api.py new file mode 100644 index 0000000000..ceacfb9862 --- /dev/null +++ b/apps/permissions/api.py @@ -0,0 +1,70 @@ +from django.http import Http404 +from django.contrib.auth.models import User +from django.contrib.auth.models import Group +from django.db.utils import DatabaseError +from django.shortcuts import get_object_or_404 +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext + +from models import Permission, Role + +class Unauthorized(Exception): + pass + + +def register_permissions(namespace, permissions): + if permissions: + for permission in permissions: + try: + permission_obj, created = Permission.objects.get_or_create( + namespace=namespace, name=permission['name']) + permission_obj.label=unicode(permission['label']) + permission_obj.save() + except DatabaseError: + #Special case for ./manage.py syncdb + pass + +#TODO: Handle anonymous users +def check_permissions(requester, namespace, permission_list): + if isinstance(requester, User): + if requester.is_superuser: + return True + + for permission_item in permission_list: + permission = get_object_or_404(Permission, + namespace=namespace, name=permission_item) + if check_permission(requester, permission): + return True + + raise Unauthorized(ugettext(u'Insufficient permissions.')) + + +def check_permission(requester, permission): + for permission_holder in permission.permissionholder_set.all(): + if check_requester(requester, permission_holder): + return True + + +def check_requester(requester, permission_holder): + ct = ContentType.objects.get_for_model(requester) + if permission_holder.holder_type == ct and permission_holder.holder_id == requester.id: + return True + + if isinstance(permission_holder.holder_object, Role): + requester_list = [role_member.member_object for role_member in permission_holder.holder_object.rolemember_set.all()] + if check_elements(requester, requester_list): + return True + + #Untested + if isinstance(permission_holder.holder_object, Group): + if check_elements(requester, permission_holder.holder_object.user_set.all()): + return True + + +#TODO: a role may contain groups, make recursive +def check_elements(requester, requester_list): + ct = ContentType.objects.get_for_model(requester) + for requester_object in requester_list: + if requester == requester_object: + return True diff --git a/apps/permissions/conf/__init__.py b/apps/permissions/conf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/permissions/conf/settings.py b/apps/permissions/conf/settings.py new file mode 100644 index 0000000000..af5146cc0f --- /dev/null +++ b/apps/permissions/conf/settings.py @@ -0,0 +1,3 @@ +from django.conf import settings + +DEFAULT_ROLES = getattr(settings, 'ROLES_DEFAULT_ROLES', []) diff --git a/apps/permissions/locale/es/LC_MESSAGES/django.po b/apps/permissions/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000..e60738cb1f --- /dev/null +++ b/apps/permissions/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,67 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-02-13 04:12-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: api.py:40 +msgid "Insufficient permissions." +msgstr "" + +#: models.py:8 +msgid "namespace" +msgstr "" + +#: models.py:9 +msgid "name" +msgstr "" + +#: models.py:10 models.py:40 +msgid "label" +msgstr "" + +#: models.py:14 models.py:23 +msgid "permission" +msgstr "" + +#: models.py:15 +msgid "permissions" +msgstr "" + +#: models.py:31 +msgid "permission holder" +msgstr "" + +#: models.py:32 +msgid "permission holders" +msgstr "" + +#: models.py:44 models.py:58 +msgid "role" +msgstr "" + +#: models.py:45 +msgid "roles" +msgstr "" + +#: models.py:67 +msgid "role member" +msgstr "" + +#: models.py:68 +msgid "role members" +msgstr "" diff --git a/apps/permissions/models.py b/apps/permissions/models.py new file mode 100644 index 0000000000..1acf0e7896 --- /dev/null +++ b/apps/permissions/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic + + +class Permission(models.Model): + namespace = models.CharField(max_length=64, verbose_name=_(u'namespace')) + name = models.CharField(max_length=64, verbose_name=_(u'name')) + label = models.CharField(max_length=64, verbose_name=_(u'label')) + + class Meta: + unique_together = ('namespace', 'name') + verbose_name = _(u'permission') + verbose_name_plural = _(u'permissions') + + + def __unicode__(self): + return self.label + + +class PermissionHolder(models.Model): + permission = models.ForeignKey(Permission, verbose_name=_(u'permission')) + holder_type = models.ForeignKey(ContentType, + related_name='permission_holder', + limit_choices_to = {'model__in': ('user', 'group', 'role')}) + holder_id = models.PositiveIntegerField() + holder_object = generic.GenericForeignKey(ct_field='holder_type', fk_field='holder_id') + + class Meta: + verbose_name = _(u'permission holder') + verbose_name_plural = _(u'permission holders') + + def __unicode__(self): + return unicode(self.holder_object) + + +class Role(models.Model): + name = models.CharField(max_length=64, unique=True) + label = models.CharField(max_length=64, unique=True, verbose_name=_(u'label')) + + class Meta: + ordering = ('label',) + verbose_name = _(u'role') + verbose_name_plural = _(u'roles') + + def add_member(self, member): + role_member, created = RoleMember.objects.get_or_create( + role=self, + member_type = ContentType.objects.get_for_model(member), + member_id=member.id) + + def __unicode__(self): + return self.label + + +class RoleMember(models.Model): + role = models.ForeignKey(Role, verbose_name=_(u'role')) + member_type = models.ForeignKey(ContentType, + related_name='role_member', + limit_choices_to = {'model__in': ('user', 'group', 'role')}) + member_id = models.PositiveIntegerField() + member_object = generic.GenericForeignKey(ct_field='member_type', fk_field='member_id') + + class Meta: + #ordering = ('label',) + verbose_name = _(u'role member') + verbose_name_plural = _(u'role members') + + def __unicode__(self): + return unicode(self.member_object) diff --git a/apps/permissions/templatetags/__init__.py b/apps/permissions/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/permissions/templatetags/permission_tags.py b/apps/permissions/templatetags/permission_tags.py new file mode 100644 index 0000000000..d4168af744 --- /dev/null +++ b/apps/permissions/templatetags/permission_tags.py @@ -0,0 +1,37 @@ +from django.template import TemplateSyntaxError, Library, \ + VariableDoesNotExist, Node, Variable + +from permissions.api import check_permissions as check_permission_function, Unauthorized + +register = Library() + + +class CheckPermissionsNode(Node): + def __init__(self, requester, namespace, permission_list, *args, **kwargs): + self.requester = requester + self.namespace = namespace + self.permission_list = permission_list + + def render(self, context): + requester = Variable(self.requester).resolve(context) + namespace = Variable(self.namespace).resolve(context) + permission_list = Variable(self.permission_list).resolve(context) + try: + check_permission_function(requester, namespace, permission_list) + context['permission'] = True + return '' + except Unauthorized: + context['permission'] = False + return '' + + +@register.tag +def check_permissions(parser, token): + try: + # Splitting by None == splitting by spaces. + tag_name, args = token.contents.split(None, 1) + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires arguments" % token.contents.split()[0] + + return CheckPermissionsNode(*args.split()) + diff --git a/apps/permissions/tests.py b/apps/permissions/tests.py new file mode 100644 index 0000000000..2247054b35 --- /dev/null +++ b/apps/permissions/tests.py @@ -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 +"""} + diff --git a/apps/permissions/views.py b/apps/permissions/views.py new file mode 100644 index 0000000000..60f00ef0ef --- /dev/null +++ b/apps/permissions/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/docs/Changelog.txt b/docs/Changelog.txt index f6cf3e2542..a4dfe3cb74 100644 --- a/docs/Changelog.txt +++ b/docs/Changelog.txt @@ -5,3 +5,4 @@ * Changed to a liquid css grid * Added the ability to group documents by their metadata * New abstracted options to adjust document conversion quality (default, low, high) +* Added permissions and roles support diff --git a/docs/TODO b/docs/TODO index a294dc8f9f..c26f8a18ec 100755 --- a/docs/TODO +++ b/docs/TODO @@ -26,6 +26,8 @@ * If theres only one document type on db skip step 1 of wizard - DONE * Be able to delete staging file - DONE * Group documents by metadata - DONE +* Permissions - DONE +* Roles - DONE * Document list filtering by metadata * Filterform date filtering widget * Validate GET data before saving file @@ -42,8 +44,6 @@ * MuliThreading deferred OCR * Versioning support * Generic document anotations using layer overlays -* Permissions -* Roles * Workflows * Scheduled maintenance (cleanup, deferred OCR's) * Add tags to documents @@ -60,6 +60,6 @@ * Support spreadsheets, wordprocessing docs using openoffice in server mode * WebDAV support * Handle ziped or rar archives -* Display preferences (Rotation, default zoom) +* Display preferences 'document transformations' (Rotation, default zoom) * Gallery view for document groups * Assign default role to new users diff --git a/runserver_plus.sh b/runserver_plus.sh index 5be5b13db6..949bd57a0e 100755 --- a/runserver_plus.sh +++ b/runserver_plus.sh @@ -1,6 +1,6 @@ #!/bin/sh if [ -n "$1" ]; then - ./manage.py runserver_plus $1 --adminmedia ./site_media/admin_media/ + ./manage.py runserver_plus $1 --adminmedia ./site_media/grappelli/ else - ./manage.py runserver_plus --adminmedia ./site_media/admin_media/ + ./manage.py runserver_plus --adminmedia ./site_media/grappelli/ fi diff --git a/settings.py b/settings.py index fe285046ce..fc425d2f54 100755 --- a/settings.py +++ b/settings.py @@ -77,7 +77,7 @@ MEDIA_URL = '/%s-site_media/' % PROJECT_NAME # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = MEDIA_URL + 'admin_media/' +ADMIN_MEDIA_PREFIX = MEDIA_URL + 'grappelli/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'om^a(i8^6&h+umbd2%pt91cj!qu_@oztw117rgxmn(n2lp^*c!' @@ -215,6 +215,9 @@ LOGIN_EXEMPT_URLS = ( # OCR #OCR_TESSERACT_PATH = u'/usr/bin/tesseract' +# Permissions +#ROLES_DEFAULT_ROLES = [] + # Override SEARCH_SHOW_OBJECT_TYPE = False #======== End of configuration options ======= diff --git a/site_media/admin_media b/site_media/grappelli similarity index 100% rename from site_media/admin_media rename to site_media/grappelli diff --git a/urls.py b/urls.py index 979a4397ca..e609835e84 100755 --- a/urls.py +++ b/urls.py @@ -12,6 +12,7 @@ urlpatterns = patterns('', (r'^ocr/', include('ocr.urls')), (r'^admin/doc/', include('django.contrib.admindocs.urls')), (r'^admin/', include(admin.site.urls)), + (r'^grappelli/', include('grappelli.urls')), ) if settings.DEVELOPMENT: