Make folders and tags apps multitenant.

This commit is contained in:
Roberto Rosario
2016-03-07 01:53:13 -04:00
parent 378a346e79
commit 6492908c59
29 changed files with 661 additions and 66 deletions

View File

@@ -70,7 +70,7 @@
{% if not user.is_authenticated %}
{% trans 'Anonymous' %}
{% else %}
<li><a href="{% url 'common:current_user_details' %}" title="{% trans 'User details' %}">{{ user.get_full_name|default:user }} <i class="fa fa-user"></i></a></li>
<li><a href="{% url 'common:current_user_details' %}" title="{% trans 'User details' %}">{{ request.organization }}: {{ user.get_full_name|default:user }} <i class="fa fa-user"></i></a></li>
{% endif %}
</ul>
</div>

View File

@@ -104,7 +104,7 @@ class DocumentTypeSelectForm(forms.Form):
as form #1 in the document creation wizard
"""
document_type = forms.ModelChoiceField(
queryset=DocumentType.objects.all(), label=_('Document type')
queryset=DocumentType.on_organization.all(), label=_('Document type')
)

View File

@@ -4,6 +4,7 @@ from datetime import timedelta
import logging
from django.apps import apps
from django.conf import settings
from django.db import models
from django.utils.timezone import now
@@ -132,7 +133,8 @@ class RecentDocumentManager(models.Manager):
if user.is_authenticated():
return document_model.objects.filter(
recentdocument__user=user
recentdocument__user=user,
document_type__organization__id=settings.ORGANIZATION_ID
).order_by('-recentdocument__datetime_accessed')
else:
return document_model.objects.none()

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
('documents', '0028_newversionblock'),
]
operations = [
migrations.AddField(
model_name='documenttype',
name='organization',
field=models.ForeignKey(default=1, to='organizations.Organization'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
('documents', '0029_documenttype_organization'),
]
operations = [
migrations.AddField(
model_name='document',
name='organization',
field=models.ForeignKey(default=1, to='organizations.Organization'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('documents', '0030_document_organization'),
]
operations = [
migrations.RemoveField(
model_name='document',
name='organization',
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import organizations.shortcuts
class Migration(migrations.Migration):
dependencies = [
('documents', '0031_remove_document_organization'),
]
operations = [
migrations.AlterField(
model_name='documenttype',
name='organization',
field=models.ForeignKey(default=organizations.shortcuts.get_current_organization, to='organizations.Organization'),
preserve_default=True,
),
]

View File

@@ -24,6 +24,9 @@ from converter.exceptions import InvalidOfficeFormat, PageCountError
from converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION
from converter.models import Transformation
from mimetype.api import get_mimetype
from organizations.models import Organization
from organizations.managers import CurrentOrganizationManager
from organizations.shortcuts import get_current_organization
from permissions import Permission
from .events import (
@@ -62,6 +65,9 @@ class DocumentType(models.Model):
Define document types or classes to which a specific set of
properties can be attached
"""
organization = models.ForeignKey(
Organization, default=get_current_organization
)
label = models.CharField(
max_length=32, unique=True, verbose_name=_('Label')
)
@@ -87,6 +93,7 @@ class DocumentType(models.Model):
)
objects = DocumentTypeManager()
on_organization = CurrentOrganizationManager()
def __str__(self):
return self.label

View File

@@ -72,7 +72,7 @@ class DocumentListView(SingleObjectListView):
object_permission = permission_document_view
def get_document_queryset(self):
return Document.objects.all()
return Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
def get_queryset(self):
self.queryset = self.get_document_queryset().filter(is_stub=False)
@@ -88,7 +88,7 @@ class DeletedDocumentListView(DocumentListView):
}
def get_document_queryset(self):
queryset = Document.trash.all()
queryset = Document.trash.filter(document_type__organization__id=settings.ORGANIZATION_ID)
try:
Permission.check_permissions(
@@ -111,7 +111,7 @@ class DeletedDocumentDeleteView(ConfirmView):
def object_action(self, instance):
source_document = get_object_or_404(
Document.passthrough, pk=instance.pk
Document.passthrough.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=instance.pk
)
try:
@@ -139,14 +139,15 @@ class DeletedDocumentDeleteManyView(MultipleInstanceActionMixin, DeletedDocument
extra_context = {
'title': _('Delete the selected documents?')
}
model = DeletedDocument
success_message = '%(count)d document deleted.'
success_message_plural = '%(count)d documents deleted.'
def get_queryset(self):
return DeletedDocument.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentEditView(SingleObjectEditView):
form_class = DocumentForm
model = Document
object_permission = permission_document_properties_edit
def dispatch(self, request, *args, **kwargs):
@@ -162,16 +163,19 @@ class DocumentEditView(SingleObjectEditView):
'title': _('Edit properties of document: %s') % self.get_object(),
}
def get_save_extra_data(self):
return {
'_user': self.request.user
}
def get_queryset(self):
return Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
def get_post_action_redirect(self):
return reverse(
'documents:document_properties', args=(self.get_object().pk,)
)
def get_save_extra_data(self):
return {
'_user': self.request.user
}
class DocumentRestoreView(ConfirmView):
extra_context = {
@@ -180,7 +184,7 @@ class DocumentRestoreView(ConfirmView):
def object_action(self, instance):
source_document = get_object_or_404(
Document.passthrough, pk=instance.pk
Document.passthrough.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=instance.pk
)
try:
@@ -210,10 +214,12 @@ class DocumentRestoreManyView(MultipleInstanceActionMixin, DocumentRestoreView):
extra_context = {
'title': _('Restore the selected documents?')
}
model = DeletedDocument
success_message = '%(count)d document restored.'
success_message_plural = '%(count)d documents restored.'
def get_queryset(self):
return DeletedDocument.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentPageListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
@@ -232,7 +238,7 @@ class DocumentPageListView(SingleObjectListView):
).dispatch(request, *args, **kwargs)
def get_document(self):
return get_object_or_404(Document, pk=self.kwargs['pk'])
return get_object_or_404(Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
def get_queryset(self):
return self.get_document().pages.all()
@@ -287,11 +293,10 @@ class DocumentPageView(SimpleView):
}
def get_object(self):
return get_object_or_404(DocumentPage, pk=self.kwargs['pk'])
return get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__pk=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
class DocumentPreviewView(SingleObjectDetailView):
model = Document
object_permission = permission_document_view
def dispatch(self, request, *args, **kwargs):
@@ -309,6 +314,9 @@ class DocumentPreviewView(SingleObjectDetailView):
'title': _('Preview of document: %s') % self.get_object(),
}
def get_queryset(self):
return Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentTrashView(ConfirmView):
def get_extra_context(self):
@@ -318,7 +326,7 @@ class DocumentTrashView(ConfirmView):
}
def get_object(self):
return get_object_or_404(Document, pk=self.kwargs['pk'])
return get_object_or_404(Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
def get_post_action_redirect(self):
return reverse('documents:document_list_recent')
@@ -348,7 +356,6 @@ class DocumentTrashView(ConfirmView):
class DocumentTrashManyView(MultipleInstanceActionMixin, DocumentTrashView):
model = Document
success_message = '%(count)d document moved to the trash.'
success_message_plural = '%(count)d documents moved to the trash.'
@@ -357,10 +364,13 @@ class DocumentTrashManyView(MultipleInstanceActionMixin, DocumentTrashView):
'title': _('Move the selected documents to the trash?')
}
def get_queryset(self):
return Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentTypeDocumentListView(DocumentListView):
def get_document_type(self):
return get_object_or_404(DocumentType, pk=self.kwargs['pk'])
return get_object_or_404(DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
def get_document_queryset(self):
return self.get_document_type().documents.all()
@@ -374,7 +384,6 @@ class DocumentTypeDocumentListView(DocumentListView):
class DocumentTypeListView(SingleObjectListView):
model = DocumentType
view_permission = permission_document_type_view
def get_extra_context(self):
@@ -383,13 +392,15 @@ class DocumentTypeListView(SingleObjectListView):
'title': _('Document types'),
}
def get_queryset(self):
return DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID)
class DocumentTypeCreateView(SingleObjectCreateView):
fields = (
'label', 'trash_time_period', 'trash_time_unit', 'delete_time_period',
'delete_time_unit'
)
model = DocumentType
post_action_redirect = reverse_lazy('documents:document_type_list')
view_permission = permission_document_type_create
@@ -398,9 +409,11 @@ class DocumentTypeCreateView(SingleObjectCreateView):
'title': _('Create document type'),
}
def get_queryset(self):
return DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID)
class DocumentTypeDeleteView(SingleObjectDeleteView):
model = DocumentType
post_action_redirect = reverse_lazy('documents:document_type_list')
view_permission = permission_document_type_delete
@@ -411,13 +424,15 @@ class DocumentTypeDeleteView(SingleObjectDeleteView):
'title': _('Delete the document type: %s?') % self.get_object(),
}
def get_queryset(self):
return DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID)
class DocumentTypeEditView(SingleObjectEditView):
fields = (
'label', 'trash_time_period', 'trash_time_unit', 'delete_time_period',
'delete_time_unit'
)
model = DocumentType
post_action_redirect = reverse_lazy('documents:document_type_list')
view_permission = permission_document_type_edit
@@ -427,13 +442,15 @@ class DocumentTypeEditView(SingleObjectEditView):
'title': _('Edit document type: %s') % self.get_object(),
}
def get_queryset(self):
return DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID)
class DocumentTypeFilenameListView(SingleObjectListView):
model = DocumentType
view_permission = permission_document_type_view
def get_document_type(self):
return get_object_or_404(DocumentType, pk=self.kwargs['pk'])
return get_object_or_404(DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
def get_extra_context(self):
return {
@@ -451,7 +468,6 @@ class DocumentTypeFilenameListView(SingleObjectListView):
class DocumentTypeFilenameEditView(SingleObjectEditView):
fields = ('enabled', 'filename',)
model = DocumentTypeFilename
view_permission = permission_document_type_edit
def get_extra_context(self):
@@ -476,9 +492,11 @@ class DocumentTypeFilenameEditView(SingleObjectEditView):
args=(self.get_object().document_type.pk,)
)
def get_queryset(self):
return DocumentTypeFilename.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentTypeFilenameDeleteView(SingleObjectDeleteView):
model = DocumentTypeFilename
view_permission = permission_document_type_edit
def get_extra_context(self):
@@ -501,6 +519,9 @@ class DocumentTypeFilenameDeleteView(SingleObjectDeleteView):
args=(self.get_object().document_type.pk,)
)
def get_queryset(self):
return DocumentTypeFilename.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class DocumentVersionListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
@@ -520,7 +541,7 @@ class DocumentVersionListView(SingleObjectListView):
).dispatch(request, *args, **kwargs)
def get_document(self):
return get_object_or_404(Document, pk=self.kwargs['pk'])
return get_object_or_404(Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=self.kwargs['pk'])
def get_extra_context(self):
return {
@@ -533,7 +554,6 @@ class DocumentVersionListView(SingleObjectListView):
class DocumentView(SingleObjectDetailView):
model = Document
object_permission = permission_document_view
def dispatch(self, request, *args, **kwargs):
@@ -592,6 +612,9 @@ class DocumentView(SingleObjectDetailView):
instance=document, extra_fields=document_fields
)
def get_queryset(self):
return Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
class EmptyTrashCanView(ConfirmView):
extra_context = {
@@ -603,7 +626,7 @@ class EmptyTrashCanView(ConfirmView):
)
def view_action(self):
for deleted_document in DeletedDocument.objects.all():
for deleted_document in DeletedDocument.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID):
deleted_document.delete()
messages.success(self.request, _('Trash emptied successfully'))
@@ -622,11 +645,13 @@ class RecentDocumentListView(DocumentListView):
def document_document_type_edit(request, document_id=None, document_id_list=None):
post_action_redirect = None
queryset = Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
if document_id:
queryset = Document.objects.filter(pk=document_id)
queryset = queryset.filter(pk=document_id)
post_action_redirect = reverse('documents:document_list_recent')
elif document_id_list:
queryset = Document.objects.filter(pk__in=document_id_list)
queryset = queryset.filter(pk__in=document_id_list)
try:
Permission.check_permissions(
@@ -700,7 +725,7 @@ def document_multiple_document_type_edit(request):
# TODO: Get rid of this view and convert widget to use API and base64 only images
def get_document_image(request, document_id, size=setting_preview_size.value):
document = get_object_or_404(Document.passthrough, pk=document_id)
document = get_object_or_404(Document.passthrough.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=document_id)
try:
Permission.check_permissions(request.user, (permission_document_view,))
except PermissionDenied:
@@ -732,12 +757,14 @@ def get_document_image(request, document_id, size=setting_preview_size.value):
def document_download(request, document_id=None, document_id_list=None, document_version_pk=None):
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
queryset = Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
if document_id:
documents = Document.objects.filter(pk=document_id)
documents = queryset.filter(pk=document_id)
elif document_id_list:
documents = Document.objects.filter(pk__in=document_id_list)
documents = queryset.filter(pk__in=document_id_list)
elif document_version_pk:
documents = Document.objects.filter(
documents = queryset.filter(
pk=get_object_or_404(
DocumentVersion, pk=document_version_pk
).document.pk
@@ -762,6 +789,7 @@ def document_download(request, document_id=None, document_id_list=None, document
)
)
# TODO: check organization
if document_version_pk:
queryset = DocumentVersion.objects.filter(pk=document_version_pk)
else:
@@ -873,10 +901,12 @@ def document_multiple_download(request):
def document_update_page_count(request, document_id=None, document_id_list=None):
queryset = Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
if document_id:
documents = Document.objects.filter(pk=document_id)
documents = queryset.filter(pk=document_id)
elif document_id_list:
documents = Document.objects.filter(pk__in=document_id_list)
documents = queryset.objects.filter(pk__in=document_id_list)
if not documents:
messages.error(request, _('At least one document must be selected.'))
@@ -936,11 +966,13 @@ def document_multiple_update_page_count(request):
def document_clear_transformations(request, document_id=None, document_id_list=None):
queryset = Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID)
if document_id:
documents = Document.objects.filter(pk=document_id)
documents = queryset.filter(pk=document_id)
post_redirect = documents[0].get_absolute_url()
elif document_id_list:
documents = Document.objects.filter(pk__in=document_id_list)
documents = queryset.filter(pk__in=document_id_list)
post_redirect = None
if not documents:
@@ -1020,7 +1052,7 @@ def document_page_view_reset(request, document_page_id):
def document_page_navigation_next(request, document_page_id):
document_page = get_object_or_404(DocumentPage, pk=document_page_id)
document_page = get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_page_id)
try:
Permission.check_permissions(request.user, (permission_document_view,))
@@ -1038,7 +1070,7 @@ def document_page_navigation_next(request, document_page_id):
def document_page_navigation_previous(request, document_page_id):
document_page = get_object_or_404(DocumentPage, pk=document_page_id)
document_page = get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_page_id)
try:
Permission.check_permissions(request.user, (permission_document_view,))
@@ -1056,7 +1088,7 @@ def document_page_navigation_previous(request, document_page_id):
def document_page_navigation_first(request, document_page_id):
document_page = get_object_or_404(DocumentPage, pk=document_page_id)
document_page = get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_page_id)
document_page = get_object_or_404(document_page.siblings, page_number=1)
try:
@@ -1070,7 +1102,7 @@ def document_page_navigation_first(request, document_page_id):
def document_page_navigation_last(request, document_page_id):
document_page = get_object_or_404(DocumentPage, pk=document_page_id)
document_page = get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_page_id)
document_page = get_object_or_404(document_page.siblings, page_number=document_page.siblings.count())
try:
@@ -1084,7 +1116,7 @@ def document_page_navigation_last(request, document_page_id):
def transform_page(request, document_page_id, zoom_function=None, rotation_function=None):
document_page = get_object_or_404(DocumentPage, pk=document_page_id)
document_page = get_object_or_404(DocumentPage.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_page_id)
try:
Permission.check_permissions(request.user, (permission_document_view,))
@@ -1147,7 +1179,7 @@ def document_page_rotate_left(request, document_page_id):
def document_print(request, document_id):
document = get_object_or_404(Document, pk=document_id)
document = get_object_or_404(Document.objects.filter(document_type__organization__id=settings.ORGANIZATION_ID), pk=document_id)
try:
Permission.check_permissions(request.user, (permission_document_print,))
@@ -1194,7 +1226,7 @@ def document_print(request, document_id):
def document_type_filename_create(request, document_type_id):
Permission.check_permissions(request.user, (permission_document_type_edit,))
document_type = get_object_or_404(DocumentType, pk=document_type_id)
document_type = get_object_or_404(DocumentType.objects.filter(organization__id=settings.ORGANIZATION_ID), pk=document_type_id)
if request.method == 'POST':
form = DocumentTypeFilenameForm_create(request.POST)
@@ -1240,7 +1272,7 @@ def document_clear_image_cache(request):
def document_version_revert(request, document_version_pk):
document_version = get_object_or_404(DocumentVersion, pk=document_version_pk)
document_version = get_object_or_404(DocumentVersion.objects.filter(document__document_type__organization__id=settings.ORGANIZATION_ID), pk=document_version_pk)
try:
Permission.check_permissions(request.user, (permission_document_version_revert,))

View File

@@ -21,7 +21,7 @@ class FolderListForm(forms.Form):
logger.debug('user: %s', user)
super(FolderListForm, self).__init__(*args, **kwargs)
queryset = Folder.objects.all()
queryset = Folder.on_organization.all()
try:
Permission.check_permissions(user, (permission_folder_view,))
except PermissionDenied:

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import organizations.shortcuts
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
('folders', '0004_documentfolder'),
]
operations = [
migrations.AddField(
model_name='folder',
name='organization',
field=models.ForeignKey(default=organizations.shortcuts.get_current_organization, to='organizations.Organization'),
preserve_default=True,
),
]

View File

@@ -10,6 +10,9 @@ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view
from organizations.models import Organization
from organizations.managers import CurrentOrganizationManager
from organizations.shortcuts import get_current_organization
from permissions import Permission
from .managers import FolderManager
@@ -17,6 +20,9 @@ from .managers import FolderManager
@python_2_unicode_compatible
class Folder(models.Model):
organization = models.ForeignKey(
Organization, default=get_current_organization
)
label = models.CharField(
db_index=True, max_length=128, verbose_name=_('Label')
)
@@ -29,6 +35,7 @@ class Folder(models.Model):
)
objects = FolderManager()
on_organization = CurrentOrganizationManager()
def __str__(self):
return self.label

View File

@@ -33,7 +33,6 @@ logger = logging.getLogger(__name__)
class FolderEditView(SingleObjectEditView):
fields = ('label',)
model = Folder
object_permission = permission_folder_edit
post_action_redirect = reverse_lazy('folders:folder_list')
@@ -43,6 +42,9 @@ class FolderEditView(SingleObjectEditView):
'title': _('Edit folder: %s') % self.get_object(),
}
def get_document_queryset(self):
return Folder.on_organization.all()
class FolderListView(SingleObjectListView):
object_permission = permission_folder_view
@@ -54,7 +56,7 @@ class FolderListView(SingleObjectListView):
}
def get_folder_queryset(self):
return Folder.objects.all()
return Folder.on_organization.all()
def get_queryset(self):
self.queryset = self.get_folder_queryset()
@@ -63,7 +65,6 @@ class FolderListView(SingleObjectListView):
class FolderCreateView(SingleObjectCreateView):
fields = ('label',)
model = Folder
view_permission = permission_folder_create
def form_valid(self, form):
@@ -90,9 +91,12 @@ class FolderCreateView(SingleObjectCreateView):
'title': _('Create folder'),
}
def get_queryset(self):
return Folder.on_organization.all()
def folder_delete(request, folder_id):
folder = get_object_or_404(Folder, pk=folder_id)
folder = get_object_or_404(Folder.on_organization, pk=folder_id)
try:
Permission.check_permissions(request.user, (permission_folder_delete,))
@@ -142,7 +146,7 @@ class FolderDetailView(DocumentListView):
}
def get_folder(self):
folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
folder = get_object_or_404(Folder.on_organization, pk=self.kwargs['pk'])
try:
Permission.check_permissions(
@@ -267,7 +271,7 @@ class DocumentFolderListView(FolderListView):
def folder_document_remove(request, folder_id, document_id=None, document_id_list=None):
post_action_redirect = None
folder = get_object_or_404(Folder, pk=folder_id)
folder = get_object_or_404(Folder.on_organization, pk=folder_id)
if document_id:
queryset = Document.objects.filter(pk=document_id)

View File

@@ -0,0 +1,3 @@
from __future__ import unicode_literals
default_app_config = 'organizations.apps.OrganizationApp'

View File

@@ -0,0 +1,11 @@
from __future__ import unicode_literals
from django.contrib import admin
from .models import Organization
@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
list_display = ('label',)
search_fields = ('label',)

View File

@@ -0,0 +1,12 @@
from __future__ import unicode_literals
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from common.apps import MayanAppConfig
class OrganizationApp(AppConfig):
name = 'organizations'
verbose_name = _('Organizations')

View File

@@ -0,0 +1,43 @@
"""
Creates the default Organization object.
"""
from django.apps import apps
from django.core.management.color import no_style
from django.db import DEFAULT_DB_ALIAS, connections, router
from django.db.models import signals
def create_default_organization(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
try:
Organization = apps.get_model('organizations', 'Organization')
except LookupError:
return
if not router.allow_migrate(using, Organization):
return
if not Organization.objects.using(using).exists():
# The default settings set ORGANIZATION_ID = 1, and some tests in Django's test
# suite rely on this value. However, if database sequences are reused
# (e.g. in the test suite after flush/syncdb), it isn't guaranteed that
# the next id will be 1, so we coerce it. See #15573 and #16353. This
# can also crop up outside of tests - see #15346.
if verbosity >= 2:
print("Creating default Organization object")
Organization(pk=1, label='Default').save(using=using)
# We set an explicit pk instead of relying on auto-incrementation,
# so we need to reset the database sequence. See #17415.
sequence_sql = connections[using].ops.sequence_reset_sql(no_style(), [Organization])
if sequence_sql:
if verbosity >= 2:
print('Resetting sequence')
with connections[using].cursor() as cursor:
for command in sequence_sql:
cursor.execute(command)
Organization.objects.clear_cache()
signals.post_migrate.connect(create_default_organization, sender=apps.get_app_config('organizations'))

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.core import checks
from django.db import models
from django.db.models.fields import FieldDoesNotExist
class CurrentOrganizationManager(models.Manager):
"Use this to limit objects to those associated with the current organization."
def __init__(self, field_name=None):
super(CurrentOrganizationManager, self).__init__()
self.__field_name = field_name
def check(self, **kwargs):
errors = super(CurrentOrganizationManager, self).check(**kwargs)
errors.extend(self._check_field_name())
return errors
def _check_field_name(self):
field_name = self._get_field_name()
try:
field = self.model._meta.get_field(field_name)
except FieldDoesNotExist:
return [
checks.Error(
"CurrentOrganizationManager could not find a field named '%s'." % field_name,
hint=None,
obj=self,
id='organizations.E001',
)
]
if not isinstance(field, (models.ForeignKey, models.ManyToManyField)):
return [
checks.Error(
"CurrentOrganizationManager cannot use '%s.%s' as it is not a ForeignKey or ManyToManyField." % (
self.model._meta.object_name, field_name
),
hint=None,
obj=self,
id='organizations.E002',
)
]
return []
def _get_field_name(self):
""" Return self.__field_name or 'organization' or 'organizations'. """
if not self.__field_name:
try:
self.model._meta.get_field('organization')
except FieldDoesNotExist:
self.__field_name = 'organizations'
else:
self.__field_name = 'organization'
return self.__field_name
def get_queryset(self):
return super(CurrentOrganizationManager, self).get_queryset().filter(
**{self._get_field_name() + '__id': settings.ORGANIZATION_ID})

View File

@@ -0,0 +1,10 @@
from .models import Organization
class CurrentOrganizationMiddleware(object):
"""
Middleware that sets `organization` attribute to request object.
"""
def process_request(self, request):
request.organization = Organization.objects.get_current()

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Organization',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('label', models.CharField(max_length=50, verbose_name='Label')),
],
options={
'ordering': ('label',),
'verbose_name': 'Organization',
'verbose_name_plural': 'Organizations',
},
bases=(models.Model,),
),
]

View File

@@ -0,0 +1,71 @@
from __future__ import unicode_literals
import string
import warnings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import models
from django.db.models.signals import pre_save, pre_delete
from django.utils.deprecation import RemovedInDjango19Warning
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
ORGANIZATION_CACHE = {}
class OrganizationManager(models.Manager):
def get_current(self):
"""
Returns the current ``Organization`` based on the ORGANIZATION_ID in
the project's settings. The ``Organization`` object is cached the first
time it's retrieved from the database.
"""
from django.conf import settings
try:
oid = settings.ORGANIZATION_ID
except AttributeError:
raise ImproperlyConfigured(
"You're using the Django \"organizations framework\" without "
"having set the ORGANIZATION_ID setting. Create a site in "
"your database and set the SITE_ID setting to fix this error."
)
try:
current_organization = ORGANIZATION_CACHE[oid]
except KeyError:
current_organization = self.get(pk=oid)
ORGANIZATION_CACHE[oid] = current_organization
return current_organization
def clear_cache(self):
"""Clears the ``Organization`` object cache."""
global ORGANIZATION_CACHE
ORGANIZATION_CACHE = {}
@python_2_unicode_compatible
class Organization(models.Model):
label = models.CharField(max_length=50, verbose_name=_('Label'))
objects = OrganizationManager()
class Meta:
verbose_name = _('Organization')
verbose_name_plural = _('Organizations')
ordering = ('label',)
def __str__(self):
return self.label
def clear_organization_cache(sender, **kwargs):
"""
Clears the cache (if primed) each time a organization is saved or deleted
"""
instance = kwargs['instance']
try:
del ORGANIZATION_CACHE[instance.pk]
except KeyError:
pass
pre_save.connect(clear_organization_cache, sender=Organization)
pre_delete.connect(clear_organization_cache, sender=Organization)

View File

@@ -0,0 +1,8 @@
from __future__ import unicode_literals
from django.apps import apps
def get_current_organization():
from .models import Organization
return Organization.objects.get_current().pk

View File

@@ -0,0 +1,149 @@
from __future__ import unicode_literals
import unittest
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import connections, router
from django.http import HttpRequest
from django.test import TestCase, modify_settings, override_settings
from .management import create_default_organization
from .middleware import CurrentOrganizationMiddleware
from .models import Organization
from .shortcuts import get_current_organization
class OrganizationsFrameworkTests(TestCase):
def setUp(self):
Organization(id=settings.ORGANIZATION_ID, domain="example.com", name="example.com").save()
def test_organization_manager(self):
# Make sure that get_current() does not return a deleted Organization object.
s = Organization.objects.get_current()
self.assertTrue(isinstance(s, Organization))
s.delete()
self.assertRaises(ObjectDoesNotExist, Organization.objects.get_current)
def test_organization_cache(self):
# After updating a Organization object (e.g. via the admin), we shouldn't return a
# bogus value from the ORGANIZATION_CACHE.
organization = Organization.objects.get_current()
self.assertEqual("example.com", organization.name)
s2 = Organization.objects.get(id=settings.ORGANIZATION_ID)
s2.name = "Example organization"
s2.save()
organization = Organization.objects.get_current()
self.assertEqual("Example organization", organization.name)
def test_delete_all_organizations_clears_cache(self):
# When all organization objects are deleted the cache should also
# be cleared and get_current() should raise a DoesNotExist.
self.assertIsInstance(Organization.objects.get_current(), Organization)
Organization.objects.all().delete()
self.assertRaises(Organization.DoesNotExist, Organization.objects.get_current)
@override_settings(ALLOWED_HOSTS=['example.com'])
def test_get_current_organization(self):
# Test that the correct Organization object is returned
request = HttpRequest()
request.META = {
"SERVER_NAME": "example.com",
"SERVER_PORT": "80",
}
organization = get_current_organization(request)
self.assertTrue(isinstance(organization, Organization))
self.assertEqual(organization.id, settings.ORGANIZATION_ID)
# Test that an exception is raised if the organizations framework is installed
# but there is no matching Organization
organization.delete()
self.assertRaises(ObjectDoesNotExist, get_current_organization, request)
# A RequestOrganization is returned if the organizations framework is not installed
with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.organizations'}):
organization = get_current_organization(request)
self.assertTrue(isinstance(organization, RequestOrganization))
self.assertEqual(organization.name, "example.com")
def test_domain_name_with_whitespaces(self):
# Regression for #17320
# Domain names are not allowed contain whitespace characters
organization = Organization(name="test name", domain="test test")
self.assertRaises(ValidationError, organization.full_clean)
organization.domain = "test\ttest"
self.assertRaises(ValidationError, organization.full_clean)
organization.domain = "test\ntest"
self.assertRaises(ValidationError, organization.full_clean)
class JustOtherRouter(object):
def allow_migrate(self, db, model):
return db == 'other'
@modify_settings(INSTALLED_APPS={'append': 'django.contrib.organizations'})
class CreateDefaultOrganizationTests(TestCase):
multi_db = True
def setUp(self):
self.app_config = apps.get_app_config('organizations')
# Delete the organization created as part of the default migration process.
Organization.objects.all().delete()
def test_basic(self):
"""
#15346, #15573 - create_default_organization() creates an example organization only if
none exist.
"""
create_default_organization(self.app_config, verbosity=0)
self.assertEqual(Organization.objects.count(), 1)
create_default_organization(self.app_config, verbosity=0)
self.assertEqual(Organization.objects.count(), 1)
@unittest.skipIf('other' not in connections, "Requires 'other' database connection.")
def test_multi_db_with_router(self):
"""
#16353, #16828 - The default organization creation should respect db routing.
"""
old_routers = router.routers
router.routers = [JustOtherRouter()]
try:
create_default_organization(self.app_config, using='default', verbosity=0)
create_default_organization(self.app_config, using='other', verbosity=0)
self.assertFalse(Organization.objects.using('default').exists())
self.assertTrue(Organization.objects.using('other').exists())
finally:
router.routers = old_routers
@unittest.skipIf('other' not in connections, "Requires 'other' database connection.")
def test_multi_db(self):
create_default_organization(self.app_config, using='default', verbosity=0)
create_default_organization(self.app_config, using='other', verbosity=0)
self.assertTrue(Organization.objects.using('default').exists())
self.assertTrue(Organization.objects.using('other').exists())
def test_save_another(self):
"""
#17415 - Another organization can be created right after the default one.
On some backends the sequence needs to be reset after saving with an
explicit ID. Test that there isn't a sequence collisions by saving
another organization. This test is only meaningful with databases that use
sequences for automatic primary keys such as PostgreSQL and Oracle.
"""
create_default_organization(self.app_config, verbosity=0)
Organization(domain='example2.com', name='example2.com').save()
class MiddlewareTest(TestCase):
def test_request(self):
""" Makes sure that the request has correct `organization` attribute. """
middleware = CurrentOrganizationMiddleware()
request = HttpRequest()
middleware.process_request(request)
self.assertEqual(request.organization.id, settings.ORGANIZATION_ID)

View File

@@ -21,7 +21,7 @@ class TagListForm(forms.Form):
logger.debug('user: %s', user)
super(TagListForm, self).__init__(*args, **kwargs)
queryset = Tag.objects.all()
queryset = Tag.on_organization.all()
try:
Permission.check_permissions(user, (permission_tag_view,))
except PermissionDenied:

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import organizations.shortcuts
class Migration(migrations.Migration):
dependencies = [
('organizations', '0001_initial'),
('tags', '0006_documenttag'),
]
operations = [
migrations.AddField(
model_name='tag',
name='organization',
field=models.ForeignKey(default=organizations.shortcuts.get_current_organization, to='organizations.Organization'),
preserve_default=True,
),
]

View File

@@ -11,11 +11,17 @@ from colorful.fields import RGBColorField
from acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view
from organizations.models import Organization
from organizations.managers import CurrentOrganizationManager
from organizations.shortcuts import get_current_organization
from permissions import Permission
@python_2_unicode_compatible
class Tag(models.Model):
organization = models.ForeignKey(
Organization, default=get_current_organization
)
label = models.CharField(
db_index=True, max_length=128, unique=True, verbose_name=_('Label')
)
@@ -24,6 +30,9 @@ class Tag(models.Model):
Document, related_name='tags', verbose_name=_('Documents')
)
objects = models.Manager()
on_organization = CurrentOrganizationManager()
def __str__(self):
return self.label

View File

@@ -33,10 +33,12 @@ logger = logging.getLogger(__name__)
class TagCreateView(SingleObjectCreateView):
extra_context = {'title': _('Create tag')}
fields = ('label', 'color')
model = Tag
post_action_redirect = reverse_lazy('tags:tag_list')
view_permission = permission_tag_create
def get_queryset(self):
return Tag.on_organization.all()
def tag_attach(request, document_id=None, document_id_list=None):
if document_id:
@@ -144,17 +146,18 @@ class TagListView(SingleObjectListView):
return super(TagListView, self).get_queryset()
def get_tag_queryset(self):
return Tag.objects.all()
return Tag.on_organization.all()
def tag_delete(request, tag_id=None, tag_id_list=None):
post_action_redirect = None
queryset = Tag.on_organization.all()
if tag_id:
queryset = Tag.objects.filter(pk=tag_id)
queryset = organization.filter(pk=tag_id)
post_action_redirect = reverse('tags:tag_list')
elif tag_id_list:
queryset = Tag.objects.filter(pk__in=tag_id_list)
queryset = organization.filter(pk__in=tag_id_list)
if not queryset:
messages.error(request, _('Must provide at least one tag.'))
@@ -221,7 +224,6 @@ def tag_multiple_delete(request):
class TagEditView(SingleObjectEditView):
fields = ('label', 'color')
model = Tag
object_permission = permission_tag_edit
post_action_redirect = reverse_lazy('tags:tag_list')
@@ -231,10 +233,13 @@ class TagEditView(SingleObjectEditView):
'title': _('Edit tag: %s') % self.get_object(),
}
def get_queryset(self):
return Tag.on_organization.all()
class TagTaggedItemListView(DocumentListView):
def get_tag(self):
return get_object_or_404(Tag, pk=self.kwargs['pk'])
return get_object_or_404(Tag.on_organization, pk=self.kwargs['pk'])
def get_document_queryset(self):
return self.get_tag().documents.all()
@@ -309,17 +314,20 @@ def tag_remove(request, document_id=None, document_id_list=None, tag_id=None, ta
}
template = 'appearance/generic_confirm.html'
queryset = Tag.on_organization.all()
if tag_id:
tags = Tag.objects.filter(pk=tag_id)
tags = queryset.filter(pk=tag_id)
elif tag_id_list:
tags = Tag.objects.filter(pk__in=tag_id_list)
tags = queryset.filter(pk__in=tag_id_list)
else:
template = 'appearance/generic_form.html'
if request.method == 'POST':
form = TagListForm(request.POST, user=request.user)
if form.is_valid():
tags = Tag.objects.filter(pk=form.cleaned_data['tag'].pk)
tags = Tag.on_organization.filter(pk=form.cleaned_data['tag'].pk)
else:
if not tag_id and not tag_id_list:
form = TagListForm(user=request.user)

View File

@@ -73,6 +73,7 @@ INSTALLED_APPS = (
'lock_manager',
'mimetype',
'navigation',
'organizations',
'permissions',
'smart_settings',
'user_management',
@@ -113,6 +114,7 @@ MIDDLEWARE_CLASSES = (
'common.middleware.strip_spaces_widdleware.SpacelessMiddleware',
'authentication.middleware.login_required_middleware.LoginRequiredMiddleware',
'common.middleware.ajax_redirect.AjaxRedirect',
'organizations.middleware.CurrentOrganizationMiddleware',
)
ROOT_URLCONF = 'mayan.urls'
@@ -284,3 +286,5 @@ SWAGGER_SETTINGS = {
# ------ Timezone --------
TIMEZONE_COOKIE_NAME = 'django_timezone'
TIMEZONE_SESSION_KEY = 'django_timezone'
# ------ Organization -------
ORGANIZATION_ID = 1