Move transformation storage to the converter app via content types
This commit is contained in:
@@ -3,7 +3,20 @@ from __future__ import unicode_literals
|
||||
from django import apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common import menu_object, menu_sidebar
|
||||
|
||||
from .links import (
|
||||
link_transformation_create, link_transformation_delete,
|
||||
link_transformation_edit
|
||||
)
|
||||
from .models import Transformation
|
||||
|
||||
|
||||
class ConverterApp(apps.AppConfig):
|
||||
name = 'converter'
|
||||
verbose_name = _('Converter')
|
||||
|
||||
def ready(self):
|
||||
menu_sidebar.bind_links(links=[link_transformation_create], sources=[Transformation])
|
||||
menu_sidebar.bind_links(links=[link_transformation_create], sources=['converter:transformation_create', 'converter:transformation_list'])
|
||||
menu_object.bind_links(links=[link_transformation_edit, link_transformation_delete], sources=[Transformation])
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
from tempfile import mkstemp
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
@@ -13,8 +13,6 @@ except ImportError:
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.settings import TEMPORARY_DIRECTORY
|
||||
@@ -22,12 +20,8 @@ from common.utils import fs_cleanup
|
||||
from mimetype.api import get_mimetype
|
||||
|
||||
from .exceptions import OfficeConversionError, UnknownFileFormat
|
||||
from .literals import (
|
||||
DEFAULT_PAGE_NUMBER, DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION,
|
||||
DEFAULT_FILE_FORMAT, TRANSFORMATION_CHOICES, TRANSFORMATION_RESIZE,
|
||||
TRANSFORMATION_ROTATE, TRANSFORMATION_ZOOM, DIMENSION_SEPARATOR
|
||||
)
|
||||
from .settings import GRAPHICS_BACKEND, LIBREOFFICE_PATH
|
||||
from .literals import DEFAULT_PAGE_NUMBER, DEFAULT_FILE_FORMAT
|
||||
from .settings import LIBREOFFICE_PATH
|
||||
|
||||
CONVERTER_OFFICE_FILE_MIMETYPES = [
|
||||
'application/msword',
|
||||
@@ -111,7 +105,7 @@ class ConverterBase(object):
|
||||
readline = proc.stderr.readline()
|
||||
logger.debug('stderr: %s', readline)
|
||||
if return_code != 0:
|
||||
raise OfficeBackendError(readline)
|
||||
raise OfficeConversionError(readline)
|
||||
|
||||
filename, extension = os.path.splitext(os.path.basename(input_filepath))
|
||||
logger.debug('filename: %s', filename)
|
||||
@@ -158,7 +152,7 @@ class ConverterBase(object):
|
||||
if self.mime_type in CONVERTER_OFFICE_FILE_MIMETYPES:
|
||||
if os.path.exists(LIBREOFFICE_PATH):
|
||||
if not self.soffice_file_object:
|
||||
converted_output = Converter.soffice(self.file_object)
|
||||
converted_output = ConverterBase.soffice(self.file_object)
|
||||
self.file_object.seek(0)
|
||||
self.soffice_file_object = open(converted_output)
|
||||
self.mime_type = 'application/pdf'
|
||||
@@ -187,12 +181,16 @@ class BaseTransformation(object):
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
def get_transformations_classes(cls):
|
||||
return map(lambda name: getattr(cls, name), filter(lambda entry: entry.startswith('Transform'), dir(cls)))
|
||||
def register(cls, transformation):
|
||||
cls._registry[transformation.name] = transformation
|
||||
|
||||
@classmethod
|
||||
def get_transformations_choices(cls):
|
||||
return [(transformation.name, transformation.label) for transformation in cls.get_transformations_classes()]
|
||||
def get_transformation_choices(cls):
|
||||
return [(name, klass.label) for name, klass in cls._registry.items()]
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
return cls._registry[name]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for argument_name in self.arguments:
|
||||
@@ -206,7 +204,7 @@ class BaseTransformation(object):
|
||||
class TransformationResize(BaseTransformation):
|
||||
name = 'resize'
|
||||
arguments = ('width', 'height')
|
||||
label = _('Resize')
|
||||
label = _('Resize <width, height>')
|
||||
|
||||
def execute_on(self, *args, **kwargs):
|
||||
super(TransformationResize, self).execute_on(*args, **kwargs)
|
||||
@@ -244,7 +242,7 @@ class TransformationResize(BaseTransformation):
|
||||
class TransformationRotate(BaseTransformation):
|
||||
name = 'rotate'
|
||||
arguments = ('degrees',)
|
||||
label = _('Rotate')
|
||||
label = _('Rotate <degrees>')
|
||||
|
||||
def execute_on(self, *args, **kwargs):
|
||||
super(TransformationRotate, self).execute_on(*args, **kwargs)
|
||||
@@ -255,10 +253,15 @@ class TransformationRotate(BaseTransformation):
|
||||
class TransformationZoom(BaseTransformation):
|
||||
name = 'zoom'
|
||||
arguments = ('percent',)
|
||||
label = _('Zoom')
|
||||
label = _('Zoom <percent>')
|
||||
|
||||
def execute_on(self, *args, **kwargs):
|
||||
super(TransformationZoom, self).execute_on(*args, **kwargs)
|
||||
|
||||
decimal_value = float(self.percent) / 100
|
||||
return self.image.resize((int(self.image.size[0] * decimal_value), int(self.image.size[1] * decimal_value)), Image.ANTIALIAS)
|
||||
|
||||
|
||||
BaseTransformation.register(TransformationResize)
|
||||
BaseTransformation.register(TransformationRotate)
|
||||
BaseTransformation.register(TransformationZoom)
|
||||
|
||||
11
mayan/apps/converter/forms.py
Normal file
11
mayan/apps/converter/forms.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Transformation
|
||||
|
||||
|
||||
class TransformationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ('order', 'name', 'arguments')
|
||||
model = Transformation
|
||||
25
mayan/apps/converter/links.py
Normal file
25
mayan/apps/converter/links.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from navigation import Link
|
||||
|
||||
from .permissions import (
|
||||
PERMISSION_TRANSFORMATION_CREATE, PERMISSION_TRANSFORMATION_DELETE,
|
||||
PERMISSION_TRANSFORMATION_EDIT, PERMISSION_TRANSFORMATION_VIEW
|
||||
)
|
||||
|
||||
|
||||
def get_kwargs_factory(variable_name):
|
||||
def get_kwargs(context):
|
||||
content_type = ContentType.objects.get_for_model(context[variable_name])
|
||||
return {'app_label': '"{}"'.format(content_type.app_label), 'model': '"{}"'.format(content_type.model), 'object_id': '{}.pk'.format(variable_name)}
|
||||
|
||||
return get_kwargs
|
||||
|
||||
|
||||
link_transformation_create = Link(kwargs=get_kwargs_factory('content_object'), permissions=[PERMISSION_TRANSFORMATION_CREATE], text=_('Create new transformation'), view='converter:transformation_create')
|
||||
link_transformation_delete = Link(args='resolved_object.pk', permissions=[PERMISSION_TRANSFORMATION_DELETE], tags='dangerous', text=_('Delete'), view='converter:transformation_delete')
|
||||
link_transformation_edit = Link(args='resolved_object.pk', permissions=[PERMISSION_TRANSFORMATION_EDIT], text=_('Edit'), view='converter:transformation_edit')
|
||||
link_transformation_list = Link(kwargs=get_kwargs_factory('resolved_object'), permissions=[PERMISSION_TRANSFORMATION_VIEW], text=_('Transformations'), view='converter:transformation_list')
|
||||
42
mayan/apps/converter/managers.py
Normal file
42
mayan/apps/converter/managers.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ast import literal_eval
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
|
||||
from .classes import BaseTransformation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TransformationManager(models.Manager):
|
||||
def get_for_model(self, obj, as_classes=False):
|
||||
"""
|
||||
as_classes == True returns the transformation classes from .classes
|
||||
ready to be feed to the converter class
|
||||
"""
|
||||
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
|
||||
transformations = self.filter(content_type=content_type, object_id=obj.pk)
|
||||
|
||||
if as_classes:
|
||||
result = []
|
||||
for transformation in transformations:
|
||||
try:
|
||||
transformation_class = BaseTransformation.get(transformation.name)
|
||||
except KeyError:
|
||||
# Non existant transformation, but we don't raise an error
|
||||
logger.error('Non existant transformation: %s for %s', transformation.name, obj)
|
||||
else:
|
||||
# TODO: what to do if literal_eval fails?
|
||||
result.append(transformation_class(**literal_eval(transformation.arguments)))
|
||||
|
||||
return result
|
||||
else:
|
||||
return transformations
|
||||
|
||||
|
||||
|
||||
32
mayan/apps/converter/migrations/0001_initial.py
Normal file
32
mayan/apps/converter/migrations/0001_initial.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import converter.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transformation',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('order', models.PositiveIntegerField(default=0, null=True, verbose_name='Order', db_index=True, blank=True)),
|
||||
('transformation', models.CharField(max_length=128, verbose_name='Transformation', choices=[('rotate', 'Rotate'), ('zoom', 'Zoom'), ('resize', 'Resize')])),
|
||||
('arguments', models.TextField(blank=True, null=True, verbose_name='Arguments', validators=[converter.models.argument_validator])),
|
||||
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('order',),
|
||||
'verbose_name': 'Transformation',
|
||||
'verbose_name_plural': 'Transformations',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
19
mayan/apps/converter/migrations/0002_auto_20150608_1943.py
Normal file
19
mayan/apps/converter/migrations/0002_auto_20150608_1943.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('converter', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='transformation',
|
||||
old_name='transformation',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
||||
0
mayan/apps/converter/migrations/__init__.py
Normal file
0
mayan/apps/converter/migrations/__init__.py
Normal file
54
mayan/apps/converter/models.py
Normal file
54
mayan/apps/converter/models.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ast import literal_eval
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .classes import BaseTransformation
|
||||
from .managers import TransformationManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def argument_validator(value):
|
||||
"""
|
||||
Validates that the input evaluates correctly.
|
||||
"""
|
||||
value = value.strip()
|
||||
try:
|
||||
literal_eval(value)
|
||||
except (ValueError, SyntaxError):
|
||||
raise ValidationError(_('Enter a valid value.'), code='invalid')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Transformation(models.Model):
|
||||
"""
|
||||
Model that stores the transformation and transformation arguments
|
||||
for a given object
|
||||
"""
|
||||
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
order = models.PositiveIntegerField(default=0, blank=True, null=True, verbose_name=_('Order'), db_index=True)
|
||||
name = models.CharField(choices=BaseTransformation.get_transformation_choices(), max_length=128, verbose_name=_('Transformation'))
|
||||
arguments = models.TextField(blank=True, null=True, verbose_name=_('Arguments'), validators=[argument_validator])
|
||||
|
||||
objects = TransformationManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.get_transformation_display()
|
||||
|
||||
class Meta:
|
||||
ordering = ('order',)
|
||||
verbose_name = _('Transformation')
|
||||
verbose_name_plural = _('Transformations')
|
||||
11
mayan/apps/converter/permissions.py
Normal file
11
mayan/apps/converter/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from permissions.models import Permission, PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('converter', _('Converter'))
|
||||
PERMISSION_TRANSFORMATION_CREATE = Permission.objects.register(namespace, 'transformation_create', _('Create new transformations'))
|
||||
PERMISSION_TRANSFORMATION_DELETE = Permission.objects.register(namespace, 'transformation_delete', _('Delete transformations'))
|
||||
PERMISSION_TRANSFORMATION_EDIT = Permission.objects.register(namespace, 'transformation_edit', _('Edit transformations'))
|
||||
PERMISSION_TRANSFORMATION_VIEW = Permission.objects.register(namespace, 'transformation_view', _('View existing transformations'))
|
||||
11
mayan/apps/converter/urls.py
Normal file
11
mayan/apps/converter/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns(
|
||||
'converter.views',
|
||||
url(r'^create_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$', 'transformation_create', (), 'transformation_create'),
|
||||
url(r'^list_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$', 'transformation_list', (), 'transformation_list'),
|
||||
url(r'^delete/(?P<object_id>\d+)/$', 'transformation_delete', (), 'transformation_delete'),
|
||||
url(r'^edit/(?P<object_id>\d+)/$', 'transformation_edit', (), 'transformation_edit'),
|
||||
)
|
||||
142
mayan/apps/converter/views.py
Normal file
142
mayan/apps/converter/views.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import encapsulate
|
||||
from permissions.models import Permission
|
||||
|
||||
from .forms import TransformationForm
|
||||
from .models import Transformation
|
||||
from .permissions import (
|
||||
PERMISSION_TRANSFORMATION_CREATE, PERMISSION_TRANSFORMATION_DELETE,
|
||||
PERMISSION_TRANSFORMATION_EDIT, PERMISSION_TRANSFORMATION_VIEW
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def transformation_list(request, app_label, model, object_id):
|
||||
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
|
||||
|
||||
try:
|
||||
content_object = content_type.get_object_for_this_type(pk=object_id)
|
||||
except content_type.model_class().DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_VIEW])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_VIEW, request.user, content_object)
|
||||
|
||||
context = {
|
||||
'object_list': Transformation.objects.get_for_model(content_object),
|
||||
'content_object': content_object,
|
||||
'navigation_object_list': ['content_object'],
|
||||
'title': _('Transformations for: %s') % content_object,
|
||||
'extra_columns': [
|
||||
{'name': _('Order'), 'attribute': 'order'},
|
||||
{'name': _('Transformation'), 'attribute': encapsulate(lambda x: x.get_name_display())},
|
||||
{'name': _('Arguments'), 'attribute': 'arguments'}
|
||||
],
|
||||
'hide_link': True,
|
||||
'hide_object': True,
|
||||
}
|
||||
return render_to_response(
|
||||
'appearance/generic_list.html', context, context_instance=RequestContext(request)
|
||||
)
|
||||
|
||||
|
||||
def transformation_create(request, app_label, model, object_id):
|
||||
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
|
||||
|
||||
try:
|
||||
content_object = content_type.get_object_for_this_type(pk=object_id)
|
||||
except content_type.model_class().DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_CREATE])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_CREATE, request.user, content_object)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = TransformationForm(request.POST, initial={'content_object': content_object})
|
||||
if form.is_valid():
|
||||
instance = form.save(commit=False)
|
||||
instance.content_object = content_object
|
||||
instance.save()
|
||||
messages.success(request, _('Transformation created successfully.'))
|
||||
return HttpResponseRedirect(reverse('converter:transformation_list', args=[app_label, model, object_id]))
|
||||
else:
|
||||
form = TransformationForm(initial={'content_object': content_object})
|
||||
|
||||
return render_to_response('appearance/generic_form.html', {
|
||||
'form': form,
|
||||
'content_object': content_object,
|
||||
'navigation_object_list': ['content_object'],
|
||||
'title': _('Create new transformation for: %s') % content_object,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def transformation_delete(request, object_id):
|
||||
transformation = get_object_or_404(Transformation, pk=object_id)
|
||||
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_DELETE])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_DELETE, request.user, transformation.content_object)
|
||||
|
||||
if request.method == 'POST':
|
||||
transformation.delete()
|
||||
messages.success(request, _('Transformation deleted successfully.'))
|
||||
return HttpResponseRedirect(reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id]))
|
||||
|
||||
return render_to_response('appearance/generic_confirm.html', {
|
||||
'content_object': transformation.content_object,
|
||||
'delete_view': True,
|
||||
'navigation_object_list': ['content_object', 'transformation'],
|
||||
'previous': reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id]),
|
||||
'title': _('Are you sure you wish to delete transformation "%(transformation)s" for: %(content_object)s') % {
|
||||
'transformation': transformation,
|
||||
'content_object': transformation.content_object},
|
||||
'transformation': transformation,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def transformation_edit(request, object_id):
|
||||
transformation = get_object_or_404(Transformation, pk=object_id)
|
||||
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_EDIT])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_EDIT, request.user, transformation.content_object)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = TransformationForm(request.POST, instance=transformation)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _('Transformation edited successfully.'))
|
||||
return HttpResponseRedirect(reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id]))
|
||||
else:
|
||||
form = TransformationForm(instance=transformation)
|
||||
|
||||
return render_to_response('appearance/generic_form.html', {
|
||||
'content_object': transformation.content_object,
|
||||
'form': form,
|
||||
'navigation_object_list': ['content_object', 'transformation'],
|
||||
'title': _('Edit transformation "%(transformation)s" for: %(content_object)s') % {
|
||||
'transformation': transformation,
|
||||
'content_object': transformation.content_object},
|
||||
'transformation': transformation,
|
||||
}, context_instance=RequestContext(request))
|
||||
@@ -16,6 +16,7 @@ urlpatterns = patterns(
|
||||
url(r'^authentication/', include('authentication.urls', namespace='authentication')),
|
||||
url(r'^checkouts/', include('checkouts.urls', namespace='checkouts')),
|
||||
url(r'^comments/', include('document_comments.urls', namespace='comments')),
|
||||
url(r'^converter/', include('converter.urls', namespace='converter')),
|
||||
url(r'^document/signatures/', include('document_signatures.urls', namespace='signatures')),
|
||||
url(r'^document/states/', include('document_states.urls', namespace='document_states')),
|
||||
url(r'^documents/', include('documents.urls', namespace='documents')),
|
||||
|
||||
Reference in New Issue
Block a user