Merge branch 'master' into master_merge

This commit is contained in:
Roberto Rosario
2017-02-03 14:08:34 -04:00
35 changed files with 943 additions and 207 deletions

View File

@@ -16,6 +16,14 @@ the user links
- Stop loading theme fonts from the web (GitLab #343).
- Add support for attaching multiple tags (GitLab #307).
2.1.7 (2017-02-01)
==================
- Improved user management API endpoints.
- Improved permissions API endpoints.
- Improvements in the API tests of a few apps.
- Addition Content type list API view to the common app.
- Add API endpoints to the events app.
- Enable the parser and validation fields of the metadata serializer.
2.1.6 (2016-11-23)
=================

102
docs/releases/2.1.7.rst Normal file
View File

@@ -0,0 +1,102 @@
===============================
Mayan EDMS v2.1.7 release notes
===============================
Released: February 2, 2017
What's new
==========
This is a bug-fix release and all users are encouraged to upgrade. The focus
of this micro release was REST API improvement.
Changes
-------------
- Improved user management API endpoints (initial work by @lokeshmanmode):
- Improved user creation API endpoint to allow specifying the group
membership.
- Improved user editing API endpoint to allow specifying the group
membership.
- Improved permissions API endpoints (initial work by @lokeshmanmode):
- Add permission list API endpoint. This API endpoint lists all possible
permissions in the system.
- Improved role creation API endpoint to allow specifying the role's group
membership and role's permissions.
- Improved role editing API endpoint to allow specifying the role's group
membership and role's permissions.
- Improvements in the API tests of a few apps.
- Add content type list API view to the common app. Content type is required
when querying the events of an object, this view show list of content types
available.
- Add event type list api view. This API view shows all the possible events
that are registered in the system.
- Add event list API view. This view shows all the events that have taken
place in the system.
- Add object event list API view. This view show all the events for a specific
object (document, etc). The content type of the object whose events are being
requested must be specified. The list of available content types is provided
now by the common app API.
- The parser and validation fields of the metadata type model have been enable
in the metadata type API serializer.
Removals
--------
* None
Upgrading from a previous version
---------------------------------
Using PIP
~~~~~~~~~
Type in the console::
$ pip install -U mayan-edms
the requirements will also be updated automatically.
Using Git
~~~~~~~~~
If you installed Mayan EDMS by cloning the Git repository issue the commands::
$ git reset --hard HEAD
$ git pull
otherwise download the compressed archived and uncompress it overriding the
existing installation.
Next upgrade/add the new requirements::
$ pip install --upgrade -r requirements.txt
Common steps
~~~~~~~~~~~~
Migrate existing database schema with::
$ mayan-edms.py performupgrade
Add new static media::
$ mayan-edms.py collectstatic --noinput
The upgrade procedure is now complete.
Backward incompatible changes
=============================
* None
Bugs fixed or issues closed
===========================
* None
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -23,6 +23,7 @@ versions of the documentation contain the release notes for any later releases.
:maxdepth: 1
2.2
2.1.7
2.1.6
2.1.5
2.1.4

View File

@@ -1,8 +1,8 @@
from __future__ import unicode_literals
__title__ = 'Mayan EDMS'
__version__ = '2.1.6'
__build__ = 0x020106
__version__ = '2.1.7'
__build__ = 0x020107
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'
__description__ = 'Free Open Source Electronic Document Management System'

View File

@@ -11,7 +11,7 @@ from documents.permissions import permission_document_view
from documents.tests import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE
from permissions.models import Role
from permissions.tests.literals import TEST_ROLE_LABEL
from user_management.tests.literals import TEST_USER_USERNAME, TEST_GROUP
from user_management.tests.literals import TEST_USER_USERNAME, TEST_GROUP_NAME
from ..models import AccessControlList
@@ -48,7 +48,7 @@ class PermissionTestCase(BaseTestCase):
self.user = get_user_model().objects.create(
username=TEST_USER_USERNAME
)
self.group = Group.objects.create(name=TEST_GROUP)
self.group = Group.objects.create(name=TEST_GROUP_NAME)
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
self.group.user_set.add(self.user)

View File

@@ -0,0 +1,16 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import generics
from .serializers import ContentTypeSerializer
class APIContentTypeList(generics.ListAPIView):
"""
Returns a list of all the available content types.
"""
serializer_class = ContentTypeSerializer
queryset = ContentType.objects.order_by('app_label', 'model')

View File

@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.celery import app
from navigation.classes import Separator
from rest_api.classes import APIEndPoint
from .handlers import (
user_locale_profile_session_config, user_locale_profile_create
@@ -77,6 +78,8 @@ class CommonApp(MayanAppConfig):
def ready(self):
super(CommonApp, self).ready()
APIEndPoint(app=self, version_string='1')
app.conf.CELERYBEAT_SCHEDULE.update(
{
'task_delete_stale_uploads': {

View File

@@ -0,0 +1,11 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
class ContentTypeSerializer(serializers.ModelSerializer):
class Meta:
fields = ('app_label', 'id', 'model')
model = ContentType

View File

@@ -0,0 +1,11 @@
from __future__ import unicode_literals
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
class CommonAPITestCase(APITestCase):
def test_content_type_list_view(self):
response = self.client.get(reverse('rest_api:content-type-list'))
self.assertEqual(response.status_code, 200)

View File

@@ -11,8 +11,8 @@ from permissions import Permission
from permissions.models import Role
from permissions.tests.literals import TEST_ROLE_LABEL
from user_management.tests import (
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, TEST_GROUP,
TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL,
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD
)
from .base import BaseTestCase
@@ -33,7 +33,7 @@ class GenericViewTestCase(BaseTestCase):
password=TEST_USER_PASSWORD
)
self.group = Group.objects.create(name=TEST_GROUP)
self.group = Group.objects.create(name=TEST_GROUP_NAME)
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
self.group.user_set.add(self.user)
self.role.groups.add(self.group)

View File

@@ -5,6 +5,7 @@ from django.contrib.staticfiles.templatetags.staticfiles import static
from django.views.generic import RedirectView
from django.views.i18n import javascript_catalog, set_language
from api_views import APIContentTypeList
from .views import (
AboutView, CurrentUserDetailsView, CurrentUserEditView,
CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView,
@@ -67,3 +68,10 @@ urlpatterns += [
r'^set_language/$', set_language, name='set_language'
),
]
api_urls = [
url(
r'^content_types/$', APIContentTypeList.as_view(),
name='content-type-list'
),
]

View File

@@ -28,6 +28,7 @@ from events.permissions import permission_events_view
from mayan.celery import app
from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from rest_api.fields import DynamicSerializerField
from statistics.classes import StatisticNamespace, CharJSLine
from .handlers import create_default_document_type
@@ -99,39 +100,9 @@ class DocumentsApp(MayanAppConfig):
DocumentTypeFilename = self.get_model('DocumentTypeFilename')
DocumentVersion = self.get_model('DocumentVersion')
DashboardWidget(
func=new_document_pages_this_month, icon='fa fa-calendar',
label=_('New pages this month'),
link=reverse_lazy(
'statistics:statistic_detail',
args=('new-document-pages-per-month',)
)
)
DashboardWidget(
func=new_documents_this_month, icon='fa fa-calendar',
label=_('New documents this month'),
link=reverse_lazy(
'statistics:statistic_detail',
args=('new-documents-per-month',)
)
)
DashboardWidget(
icon='fa fa-file', queryset=Document.objects.all(),
label=_('Total documents'),
link=reverse_lazy('documents:document_list')
)
DashboardWidget(
icon='fa fa-book', queryset=DocumentType.objects.all(),
label=_('Document types'),
link=reverse_lazy('documents:document_type_list')
)
DashboardWidget(
icon='fa fa-trash', queryset=DeletedDocument.objects.all(),
label=_('Documents in trash'),
link=reverse_lazy('documents:document_list_deleted')
DynamicSerializerField.add_serializer(
klass=Document,
serializer_class='documents.serializers.DocumentSerializer'
)
MissingItem(

View File

@@ -131,6 +131,7 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer):
'latest_version', 'url', 'uuid', 'versions',
)
model = Document
read_only_fields = ('document_type',)
class NewDocumentSerializer(serializers.ModelSerializer):

View File

@@ -16,15 +16,18 @@ __all__ = (
'TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH',
'TEST_NON_ASCII_DOCUMENT_FILENAME', 'TEST_NON_ASCII_DOCUMENT_PATH',
'TEST_SMALL_DOCUMENT_FILENAME', 'TEST_SMALL_DOCUMENT_PATH',
'TEST_DOCUMENT_VERSION_COMMENT_EDITED',
)
# Filenames
TEST_COMPRESSED_DOCUMENTS_FILENAME = 'compressed_documents.zip'
TEST_DEU_DOCUMENT_FILENAME = 'deu_website.png'
TEST_DOCUMENT_DESCRIPTION = 'test description'
TEST_DOCUMENT_DESCRIPTION_EDITED = 'test document description edited'
TEST_DOCUMENT_FILENAME = 'mayan_11_1.pdf'
TEST_DOCUMENT_TYPE = 'test_document_type'
TEST_DOCUMENT_TYPE_2 = 'test document type 2'
TEST_DOCUMENT_VERSION_COMMENT_EDITED = 'test document version comment edited'
TEST_HYBRID_DOCUMENT = 'hybrid_text_and_image.pdf'
TEST_MULTI_PAGE_TIFF = 'multi_page.tiff'
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip'

View File

@@ -19,8 +19,10 @@ from user_management.tests.literals import (
)
from .literals import (
TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE,
TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH,
TEST_DOCUMENT_DESCRIPTION_EDITED, TEST_DOCUMENT_FILENAME,
TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE,
TEST_DOCUMENT_VERSION_COMMENT_EDITED, TEST_SMALL_DOCUMENT_FILENAME,
TEST_SMALL_DOCUMENT_PATH
)
from ..models import Document, DocumentType
@@ -113,6 +115,12 @@ class DocumentAPITestCase(APITestCase):
self.admin_user.delete()
self.document_type.delete()
def _upload_document(self):
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(
file_object=file_object,
)
def test_document_upload(self):
with open(TEST_DOCUMENT_PATH) as file_descriptor:
response = self.client.post(
@@ -293,5 +301,71 @@ class DocumentAPITestCase(APITestCase):
), mime_type='application/octet-stream; charset=utf-8'
)
def test_document_version_edit_via_patch(self):
self._upload_document()
response = self.client.patch(
reverse(
'rest_api:documentversion-detail',
args=(self.document.latest_version.pk,)
), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED}
)
self.assertEqual(response.status_code, 200)
self.document.latest_version.refresh_from_db()
self.assertEqual(self.document.versions.count(), 1)
self.assertEqual(
self.document.latest_version.comment,
TEST_DOCUMENT_VERSION_COMMENT_EDITED
)
def test_document_version_edit_via_put(self):
self._upload_document()
response = self.client.put(
reverse(
'rest_api:documentversion-detail',
args=(self.document.latest_version.pk,)
), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED}
)
self.assertEqual(response.status_code, 200)
self.document.latest_version.refresh_from_db()
self.assertEqual(self.document.versions.count(), 1)
self.assertEqual(
self.document.latest_version.comment,
TEST_DOCUMENT_VERSION_COMMENT_EDITED
)
def test_document_comment_edit_via_patch(self):
self._upload_document()
response = self.client.patch(
reverse(
'rest_api:document-detail',
args=(self.document.pk,)
), data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED}
)
self.assertEqual(response.status_code, 200)
self.document.refresh_from_db()
self.assertEqual(
self.document.description,
TEST_DOCUMENT_DESCRIPTION_EDITED
)
def test_document_comment_edit_via_put(self):
self._upload_document()
response = self.client.put(
reverse(
'rest_api:document-detail',
args=(self.document.pk,)
), data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED}
)
self.assertEqual(response.status_code, 200)
self.document.refresh_from_db()
self.assertEqual(
self.document.description,
TEST_DOCUMENT_DESCRIPTION_EDITED
)
# TODO: def test_document_set_document_type(self):
# pass

View File

@@ -0,0 +1,72 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404
from actstream.models import Action, any_stream
from rest_framework import generics
from acls.models import AccessControlList
from permissions import Permission
from rest_api.permissions import MayanPermission
from .classes import Event
from .permissions import permission_events_view
from .serializers import EventSerializer, EventTypeSerializer
class APIObjectEventListView(generics.ListAPIView):
"""
Return a list of events for the specified object.
"""
serializer_class = EventSerializer
def get_object(self):
content_type = get_object_or_404(
ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try:
return content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except content_type.model_class().DoesNotExist:
raise Http404
def get_queryset(self):
object = self.get_object()
try:
Permission.check_permissions(
self.request.user, permissions=(permission_events_view,)
)
except PermissionDenied:
AccessControlList.objects.check_access(
permission_events_view, self.request.user, object
)
return any_stream(object)
class APIEventTypeListView(generics.ListAPIView):
"""
Returns a list of all the available event types.
"""
serializer_class = EventTypeSerializer
queryset = sorted(Event.all(), key=lambda event: event.name)
class APIEventListView(generics.ListAPIView):
"""
Returns a list of all the available events.
"""
mayan_view_permissions = {'GET': (permission_events_view,)}
permission_classes = (MayanPermission,)
queryset = Action.objects.all()
serializer_class = EventSerializer

View File

@@ -4,8 +4,8 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_tools
from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .links import link_events_list
from .licenses import * # NOQA
@@ -21,6 +21,8 @@ class EventsApp(MayanAppConfig):
super(EventsApp, self).ready()
Action = apps.get_model(app_label='actstream', model_name='Action')
APIEndPoint(app=self, version_string='1')
SourceColumn(
source=Action, label=_('Timestamp'), attribute='timestamp'
)

View File

@@ -7,26 +7,48 @@ from actstream import action
class Event(object):
_labels = {}
_registry = {}
@classmethod
def all(cls):
return cls._registry.values()
@classmethod
def get(cls, name):
try:
return cls._registry[name]
except KeyError:
raise KeyError(
_('Unknown or obsolete event type: {0}'.format(name))
)
@classmethod
def get_label(cls, name):
try:
return cls._labels[name]
except KeyError:
return _('Unknown or obsolete event type: {0}'.format(name))
return cls.get(name=name).label
except KeyError as exception:
return unicode(exception)
def __init__(self, name, label):
self.name = name
self.label = label
self.event_type = None
self.__class__._labels[name] = label
self.__class__._registry[name] = self
def get_type(self):
if not self.event_type:
EventType = apps.get_model('events', 'EventType')
self.event_type, created = EventType.objects.get_or_create(
name=self.name
)
return self.event_type
def commit(self, actor=None, action_object=None, target=None):
model = apps.get_model('events', 'EventType')
if not self.event_type:
self.event_type, created = model.objects.get_or_create(
EventType = apps.get_model('events', 'EventType')
self.event_type, created = EventType.objects.get_or_create(
name=self.name
)

View File

@@ -13,9 +13,12 @@ class EventType(models.Model):
max_length=64, unique=True, verbose_name=_('Name')
)
def __str__(self):
return unicode(Event.get_label(self.name))
class Meta:
verbose_name = _('Event type')
verbose_name_plural = _('Event types')
def __str__(self):
return self.get_class().label
def get_class(self):
return Event.get_label(self.name)

View File

@@ -0,0 +1,45 @@
from __future__ import unicode_literals
from django.utils.six import string_types
from actstream.models import Action
from rest_framework import serializers
from common.serializers import ContentTypeSerializer
from rest_api.fields import DynamicSerializerField
from .classes import Event
from .models import EventType
class EventTypeSerializer(serializers.Serializer):
label = serializers.CharField()
name = serializers.CharField()
def to_representation(self, instance):
if isinstance(instance, Event):
return super(EventTypeSerializer, self).to_representation(
instance
)
elif isinstance(instance, EventType):
return super(EventTypeSerializer, self).to_representation(
instance.get_class()
)
elif isinstance(instance, string_types):
return super(EventTypeSerializer, self).to_representation(
Event.get(name=instance)
)
class EventSerializer(serializers.ModelSerializer):
actor = DynamicSerializerField(read_only=True)
target = DynamicSerializerField(read_only=True)
actor_content_type = ContentTypeSerializer(read_only=True)
target_content_type = ContentTypeSerializer(read_only=True)
verb = EventTypeSerializer(read_only=True)
class Meta:
exclude = (
'action_object_content_type', 'action_object_object_id'
)
model = Action

View File

@@ -0,0 +1,11 @@
from __future__ import unicode_literals
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
class EventAPITestCase(APITestCase):
def test_evet_type_list_view(self):
response = self.client.get(reverse('rest_api:event-type-list'))
self.assertEqual(response.status_code, 200)

View File

@@ -2,6 +2,9 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIEventListView, APIEventTypeListView, APIObjectEventListView
)
from .views import EventListView, ObjectEventListView, VerbEventListView
urlpatterns = [
@@ -15,3 +18,12 @@ urlpatterns = [
name='events_by_verb'
),
]
api_urls = [
url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'),
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
APIObjectEventListView.as_view(), name='object-event-list'
),
]

View File

@@ -9,7 +9,9 @@ from .models import DocumentMetadata, MetadataType, DocumentTypeMetadataType
class MetadataTypeSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'name', 'label', 'default', 'lookup')
fields = (
'id', 'name', 'label', 'default', 'lookup', 'parser', 'validation'
)
model = MetadataType

View File

@@ -12,5 +12,7 @@ TEST_METADATA_TYPE_LABEL = 'test metadata type'
TEST_METADATA_TYPE_LABEL_2 = 'test metadata type label 2'
TEST_METADATA_TYPE_NAME = 'test'
TEST_METADATA_TYPE_NAME_2 = 'test metadata type name 2'
TEST_METADATA_VALUE = 'test value'
TEST_METADATA_VALUE_EDITED = 'test value edited'
TEST_PARSED_VALID_DATE = '2001-01-01'
TEST_VALID_DATE = '2001-1-1'

View File

@@ -16,12 +16,10 @@ from ..models import DocumentMetadata, DocumentTypeMetadataType, MetadataType
from .literals import (
TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_LABEL_2,
TEST_METADATA_TYPE_NAME, TEST_METADATA_TYPE_NAME_2
TEST_METADATA_TYPE_NAME, TEST_METADATA_TYPE_NAME_2, TEST_METADATA_VALUE,
TEST_METADATA_VALUE_EDITED
)
TEST_METADATA_VALUE = 'test value'
TEST_METADATA_VALUE_EDITED = 'test value edited'
class MetadataTypeAPITestCase(APITestCase):
def setUp(self):
@@ -34,8 +32,10 @@ class MetadataTypeAPITestCase(APITestCase):
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
def tearDown(self):
self.admin_user.delete()
def _create_metadata_type(self):
self.metadata_type = MetadataType.objects.create(
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
)
def test_metadata_type_create(self):
response = self.client.post(
@@ -56,26 +56,35 @@ class MetadataTypeAPITestCase(APITestCase):
self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME)
def test_metadata_type_delete(self):
metadata_type = MetadataType.objects.create(
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
)
self._create_metadata_type()
response = self.client.delete(
reverse('rest_api:metadatatype-detail', args=(metadata_type.pk,))
reverse('rest_api:metadatatype-detail',
args=(self.metadata_type.pk,))
)
self.assertEqual(response.status_code, 204)
self.assertEqual(MetadataType.objects.count(), 0)
def test_metadata_type_edit(self):
metadata_type = MetadataType.objects.create(
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
def test_metadata_type_detail_view(self):
self._create_metadata_type()
response = self.client.get(
reverse('rest_api:metadatatype-detail',
args=(self.metadata_type.pk,))
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data['label'], TEST_METADATA_TYPE_LABEL
)
response = self.client.put(
reverse('rest_api:metadatatype-detail', args=(metadata_type.pk,)),
data={
def test_metadata_type_edit_via_patch_view(self):
self._create_metadata_type()
response = self.client.patch(
reverse('rest_api:metadatatype-detail',
args=(self.metadata_type.pk,)), data={
'label': TEST_METADATA_TYPE_LABEL_2,
'name': TEST_METADATA_TYPE_NAME_2
}
@@ -83,10 +92,37 @@ class MetadataTypeAPITestCase(APITestCase):
self.assertEqual(response.status_code, 200)
metadata_type.refresh_from_db()
self.metadata_type.refresh_from_db()
self.assertEqual(metadata_type.label, TEST_METADATA_TYPE_LABEL_2)
self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME_2)
self.assertEqual(self.metadata_type.label, TEST_METADATA_TYPE_LABEL_2)
self.assertEqual(self.metadata_type.name, TEST_METADATA_TYPE_NAME_2)
def test_metadata_type_edit_via_put_view(self):
self._create_metadata_type()
response = self.client.put(
reverse('rest_api:metadatatype-detail',
args=(self.metadata_type.pk,)), data={
'label': TEST_METADATA_TYPE_LABEL_2,
'name': TEST_METADATA_TYPE_NAME_2
}
)
self.assertEqual(response.status_code, 200)
self.metadata_type.refresh_from_db()
self.assertEqual(self.metadata_type.label, TEST_METADATA_TYPE_LABEL_2)
self.assertEqual(self.metadata_type.name, TEST_METADATA_TYPE_NAME_2)
def test_metadata_type_list_view(self):
self._create_metadata_type()
response = self.client.get(reverse('rest_api:metadatatype-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data['results'][0]['label'], TEST_METADATA_TYPE_LABEL
)
class DocumentTypeMetadataTypeAPITestCase(APITestCase):

View File

@@ -17,8 +17,7 @@ from .permissions import (
permission_role_view
)
from .serializers import (
PermissionSerializer, RoleNewGroupListSerializer,
RoleNewPermissionSerializer, RoleSerializer,
PermissionSerializer, RoleSerializer, WritableRoleSerializer
)
@@ -34,64 +33,12 @@ class APIPermissionList(generics.ListAPIView):
return super(APIPermissionList, self).get(*args, **kwargs)
class APIRoleGroupList(generics.ListCreateAPIView):
"""
Returns a list of all the groups that belong to selected role.
"""
mayan_object_permissions = {
'GET': (permission_role_view,),
'POST': (permission_role_edit,)
}
permission_classes = (MayanPermission,)
def get_serializer_class(self):
if self.request.method == 'GET':
return GroupSerializer
elif self.request.method == 'POST':
return RoleNewGroupListSerializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'format': self.format_kwarg,
'request': self.request,
'role': self.get_role(),
'view': self
}
def get_queryset(self):
role = self.get_role()
return AccessControlList.objects.filter_by_access(
permission_group_view, self.request.user,
queryset=role.groups.all()
)
def get_role(self):
return get_object_or_404(Role, pk=self.kwargs['pk'])
def perform_create(self, serializer):
serializer.save(role=self.get_role())
def post(self, request, *args, **kwargs):
"""
Add a list of groups to the selected role.
"""
return super(APIRoleGroupList, self).post(request, *args, **kwargs)
class APIRoleListView(generics.ListCreateAPIView):
serializer_class = RoleSerializer
queryset = Role.objects.all()
permission_classes = (MayanPermission,)
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_role_view,)}
mayan_view_permissions = {'POST': (permission_role_create,)}
permission_classes = (MayanPermission,)
queryset = Role.objects.all()
def get(self, *args, **kwargs):
"""
@@ -100,6 +47,12 @@ class APIRoleListView(generics.ListCreateAPIView):
return super(APIRoleListView, self).get(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return RoleSerializer
elif self.request.method == 'POST':
return WritableRoleSerializer
def post(self, *args, **kwargs):
"""
Create a new role.
@@ -157,16 +110,14 @@ class APIRolePermissionList(generics.ListCreateAPIView):
class APIRoleView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = RoleSerializer
queryset = Role.objects.all()
permission_classes = (MayanPermission,)
mayan_object_permissions = {
'GET': (permission_role_view,),
'PUT': (permission_role_edit,),
'PATCH': (permission_role_edit,),
'DELETE': (permission_role_delete,)
}
permission_classes = (MayanPermission,)
queryset = Role.objects.all()
def delete(self, *args, **kwargs):
"""
@@ -182,6 +133,12 @@ class APIRoleView(generics.RetrieveUpdateDestroyAPIView):
return super(APIRoleView, self).get(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return RoleSerializer
elif self.request.method in ('PATCH', 'PUT'):
return WritableRoleSerializer
def patch(self, *args, **kwargs):
"""
Edit the selected role.

View File

@@ -8,6 +8,7 @@ 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 Permission
from .managers import RoleManager, StoredPermissionManager
logger = logging.getLogger(__name__)
@@ -21,8 +22,6 @@ class StoredPermission(models.Model):
objects = StoredPermissionManager()
def __init__(self, *args, **kwargs):
from .classes import Permission
super(StoredPermission, self).__init__(*args, **kwargs)
try:
self.volatile_permission = Permission.get(

View File

@@ -28,50 +28,8 @@ class PermissionSerializer(serializers.Serializer):
)
class RoleNewGroupListSerializer(serializers.Serializer):
group_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of group primary keys to assign to a '
'selected role.'
)
)
def create(self, validated_data):
validated_data['role'].groups.clear()
try:
pk_list = validated_data['group_pk_list'].split(',')
for group in Group.objects.filter(pk__in=pk_list):
validated_data['role'].groups.add(group)
except Exception as exception:
raise ValidationError(exception)
return {'group_pk_list': validated_data['group_pk_list']}
class RoleNewPermissionSerializer(serializers.Serializer):
permission_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of permission primary keys to grant to this '
'role.'
)
)
def create(self, validated_data):
validated_data['role'].permissions.clear()
try:
for pk in validated_data['permission_pk_list'].split(','):
stored_permission = Permission.get(pk=pk)
validated_data['role'].permissions.add(stored_permission)
except KeyError as exception:
raise ValidationError(_('No such permission: %s') % pk)
except Exception as exception:
raise ValidationError(exception)
return {'permission_pk_list': validated_data['permission_pk_list']}
class RoleSerializer(serializers.HyperlinkedModelSerializer):
groups = GroupSerializer(many=True)
class RoleSerializer(serializers.ModelSerializer):
@@ -81,3 +39,77 @@ class RoleSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'label', 'groups', 'permissions')
model = Role
class WritableRoleSerializer(serializers.HyperlinkedModelSerializer):
groups_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of groups primary keys to add to, or replace'
' in this role.'
), required=False
)
permissions_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of permission primary keys to grant to this '
'role.'
), required=False
)
class Meta:
fields = ('groups_pk_list', 'id', 'label', 'permissions_pk_list')
model = Role
def create(self, validated_data):
result = validated_data.copy()
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
self.permissions_pk_list = validated_data.pop(
'permissions_pk_list', ''
)
instance = super(WritableRoleSerializer, self).create(validated_data)
if self.groups_pk_list:
self._add_groups(instance=instance)
if self.permissions_pk_list:
self._add_permissions(instance=instance)
return result
def _add_groups(self, instance):
instance.groups.add(
*Group.objects.filter(pk__in=self.groups_pk_list.split(','))
)
def _add_permissions(self, instance):
for pk in self.permissions_pk_list.split(','):
try:
stored_permission = Permission.get(pk=pk)
instance.permissions.add(stored_permission)
instance.save()
except KeyError:
raise ValidationError(_('No such permission: %s') % pk)
def update(self, instance, validated_data):
result = validated_data.copy()
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
self.permissions_pk_list = validated_data.pop(
'permissions_pk_list', ''
)
result = super(WritableRoleSerializer, self).update(
instance, validated_data
)
if self.groups_pk_list:
instance.groups.clear()
self._add_groups(instance=instance)
if self.permissions_pk_list:
instance.permissions.clear()
self._add_permissions(instance=instance)
return result

View File

@@ -0,0 +1,166 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from user_management.tests.literals import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_GROUP_NAME
)
from ..classes import Permission
from ..models import Role
from ..permissions import permission_role_view
from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED
class PermissionAPITestCase(APITestCase):
def setUp(self):
super(PermissionAPITestCase, self).setUp()
self.admin_user = get_user_model().objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
Permission.invalidate_cache()
def test_permissions_list_view(self):
response = self.client.get(reverse('rest_api:permission-list'))
self.assertEqual(response.status_code, 200)
def _create_role(self):
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
def test_roles_list_view(self):
self._create_role()
response = self.client.get(reverse('rest_api:role-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['results'][0]['label'], TEST_ROLE_LABEL)
def _role_create_request(self, extra_data=None):
data = {
'label': TEST_ROLE_LABEL
}
if extra_data:
data.update(extra_data)
return self.client.post(
reverse('rest_api:role-list'), data=data
)
def test_role_create_view(self):
response = self._role_create_request()
self.assertEqual(response.status_code, 201)
self.assertEqual(Role.objects.count(), 1)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL)
def _create_group(self):
self.group = Group.objects.create(name=TEST_GROUP_NAME)
def test_role_create_complex_view(self):
self._create_group()
response = self._role_create_request(
extra_data={
'groups_pk_list': '{}'.format(self.group.pk),
'permissions_pk_list': '{}'.format(permission_role_view.pk)
}
)
self.assertEqual(response.status_code, 201)
self.assertEqual(Role.objects.count(), 1)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL)
self.assertQuerysetEqual(
Role.objects.first().groups.all(), (repr(self.group),)
)
self.assertQuerysetEqual(
Role.objects.first().permissions.all(),
(repr(permission_role_view.stored_permission),)
)
def _role_edit_request(self, extra_data=None, request_type='patch'):
data = {
'label': TEST_ROLE_LABEL_EDITED
}
if extra_data:
data.update(extra_data)
return getattr(self.client, request_type)(
reverse('rest_api:role-detail', args=(self.role.pk,)), data=data
)
def test_role_edit_via_patch(self):
self._create_role()
response = self._role_edit_request()
self.assertEqual(response.status_code, 200)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED)
def test_role_edit_complex_via_patch(self):
Role.objects.all().delete()
Group.objects.all().delete()
self._create_role()
self._create_group()
response = self._role_edit_request(
extra_data={
'groups_pk_list': '{}'.format(self.group.pk),
'permissions_pk_list': '{}'.format(permission_role_view.pk)
}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED)
self.assertQuerysetEqual(
Role.objects.first().groups.all(), (repr(self.group),)
)
self.assertQuerysetEqual(
Role.objects.first().permissions.all(),
(repr(permission_role_view.stored_permission),)
)
def test_role_edit_via_put(self):
self._create_role()
response = self._role_edit_request(request_type='put')
self.assertEqual(response.status_code, 200)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED)
def test_role_edit_complex_via_put(self):
Role.objects.all().delete()
Group.objects.all().delete()
self._create_role()
self._create_group()
response = self._role_edit_request(
extra_data={
'groups_pk_list': '{}'.format(self.group.pk),
'permissions_pk_list': '{}'.format(permission_role_view.pk)
}, request_type='put'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED)
self.assertQuerysetEqual(
Role.objects.first().groups.all(), (repr(self.group),)
)
self.assertQuerysetEqual(
Role.objects.first().permissions.all(),
(repr(permission_role_view.stored_permission),)
)
def test_role_delete_view(self):
self._create_role()
response = self.client.delete(
reverse('rest_api:role-detail', args=(self.role.pk,))
)
self.assertEqual(response.status_code, 204)
self.assertEqual(Role.objects.count(), 0)

View File

@@ -5,7 +5,7 @@ from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from common.tests import BaseTestCase
from user_management.tests import TEST_GROUP, TEST_USER_USERNAME
from user_management.tests import TEST_GROUP_NAME, TEST_USER_USERNAME
from ..classes import Permission
from ..models import Role
@@ -20,7 +20,7 @@ class PermissionTestCase(BaseTestCase):
self.user = get_user_model().objects.create(
username=TEST_USER_USERNAME
)
self.group = Group.objects.create(name=TEST_GROUP)
self.group = Group.objects.create(name=TEST_GROUP_NAME)
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
def test_no_permissions(self):

View File

@@ -2,10 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIPermissionList, APIRoleGroupList, APIRoleListView,
APIRolePermissionList, APIRoleView,
)
from .api_views import APIPermissionList, APIRoleListView, APIRoleView
from .views import (
RoleCreateView, RoleDeleteView, RoleEditView, RoleListView,
SetupRoleMembersView, SetupRolePermissionsView
@@ -30,16 +27,8 @@ urlpatterns = [
]
api_urls = [
url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'),
url(r'^roles/$', APIRoleListView.as_view(), name='role-list'),
url(r'^roles/(?P<pk>[0-9]+)/$', APIRoleView.as_view(), name='role-detail'),
url(
r'^roles/(?P<pk>[0-9]+)/permissions/$',
APIRolePermissionList.as_view(),
name='role-permissions-list'
),
url(
r'^roles/(?P<pk>[0-9]+)/groups/$', APIRoleGroupList.as_view(),
name='role-group-list'
),
url(r'^$', APIPermissionList.as_view(), name='permission-list'),
]

View File

@@ -0,0 +1,30 @@
from __future__ import unicode_literals
from django.utils.module_loading import import_string
from django.utils.six import string_types
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
class DynamicSerializerField(serializers.ReadOnlyField):
serializers = {}
@classmethod
def add_serializer(cls, klass, serializer_class):
if isinstance(klass, string_types):
klass = import_string(klass)
if isinstance(serializer_class, string_types):
serializer_class = import_string(serializer_class)
cls.serializers[klass] = serializer_class
def to_representation(self, value):
for klass, serializer_class in self.serializers.items():
if isinstance(value, klass):
return serializer_class(
context={'request': self.context['request']}
).to_representation(instance=value)
return _('Unable to find serializer class for: %s') % value

View File

@@ -10,6 +10,7 @@ from common.widgets import two_state_template
from metadata import MetadataLookup
from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from rest_api.fields import DynamicSerializerField
from .links import (
link_group_add, link_group_delete, link_group_edit, link_group_list,
@@ -48,6 +49,10 @@ class UserManagementApp(MayanAppConfig):
User = get_user_model()
APIEndPoint(app=self, version_string='1')
DynamicSerializerField.add_serializer(
klass=get_user_model(),
serializer_class='user_management.serializers.UserSerializer'
)
MetadataLookup(
description=_('All the groups.'), name='groups',

View File

@@ -46,9 +46,13 @@ class UserGroupListSerializer(serializers.Serializer):
class UserSerializer(serializers.HyperlinkedModelSerializer):
groups = GroupSerializer(many=True, read_only=True)
groups_pk_list = serializers.CharField(
help_text=_(
'List of group primary keys to which to add the user.'
), required=False
)
password = serializers.CharField(
required=False, style={'input_type': 'password'}
required=False, style={'input_type': 'password'}, write_only=True
)
class Meta:
@@ -56,28 +60,45 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
'url': {'view_name': 'rest_api:user-detail'}
}
fields = (
'first_name', 'date_joined', 'email', 'groups', 'id', 'is_active',
'last_login', 'last_name', 'url', 'username', 'password'
'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list',
'id', 'is_active', 'last_login', 'last_name', 'password', 'url',
'username'
)
model = get_user_model()
read_only_fields = ('last_login', 'date_joined')
write_only_fields = ('password',)
read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined')
write_only_fields = ('password', 'group_pk_list')
def _add_groups(self, instance):
instance.groups.add(
*Group.objects.filter(pk__in=self.groups_pk_list.split(','))
)
def create(self, validated_data):
validated_data.pop('is_active')
user = get_user_model().objects.create_user(**validated_data)
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
password = validated_data.pop('password', None)
instance = super(UserSerializer, self).create(validated_data)
return user
if password:
instance.set_password(password)
instance.save()
if self.groups_pk_list:
self._add_groups(instance=instance)
return instance
def update(self, instance, validated_data):
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
if 'password' in validated_data:
instance.set_password(validated_data['password'])
validated_data.pop('password')
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance = super(UserSerializer, self).update(instance, validated_data)
instance.save()
if self.groups_pk_list:
instance.groups.clear()
self._add_groups(instance=instance)
return instance

View File

@@ -47,6 +47,89 @@ class UserManagementUserAPITestCase(APITestCase):
user = get_user_model().objects.get(pk=response.data['id'])
self.assertEqual(user.username, TEST_USER_USERNAME)
def test_user_create_with_group(self):
group_1 = Group.objects.create(name='test group 1')
response = self.client.post(
reverse('rest_api:user-list'), data={
'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD,
'username': TEST_USER_USERNAME,
'groups_pk_list': '{}'.format(group_1.pk)
}
)
self.assertEqual(response.status_code, 201)
user = get_user_model().objects.get(pk=response.data['id'])
self.assertEqual(user.username, TEST_USER_USERNAME)
self.assertQuerysetEqual(user.groups.all(), (repr(group_1),))
def test_user_create_with_groups(self):
group_1 = Group.objects.create(name='test group 1')
group_2 = Group.objects.create(name='test group 2')
response = self.client.post(
reverse('rest_api:user-list'), data={
'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD,
'username': TEST_USER_USERNAME,
'groups_pk_list': '{},{}'.format(group_1.pk, group_2.pk)
}
)
self.assertEqual(response.status_code, 201)
user = get_user_model().objects.get(pk=response.data['id'])
self.assertEqual(user.username, TEST_USER_USERNAME)
self.assertQuerysetEqual(
user.groups.all().order_by('name'), (repr(group_1), repr(group_2))
)
def test_user_create_login(self):
response = self.client.post(
reverse('rest_api:user-list'), data={
'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD,
'username': TEST_USER_USERNAME,
}
)
self.assertEqual(response.status_code, 201)
self.assertTrue(
self.client.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
)
)
def test_user_create_login_password_change(self):
response = self.client.post(
reverse('rest_api:user-list'), data={
'email': TEST_USER_EMAIL, 'password': 'bad_password',
'username': TEST_USER_USERNAME,
}
)
self.assertEqual(response.status_code, 201)
self.assertFalse(
self.client.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
)
)
user = get_user_model().objects.get(pk=response.data['id'])
response = self.client.patch(
reverse('rest_api:user-detail', args=(user.pk,)), data={
'password': TEST_USER_PASSWORD,
}
)
self.assertTrue(
self.client.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
)
)
def test_user_edit_via_put(self):
user = get_user_model().objects.create_user(
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
@@ -79,6 +162,44 @@ class UserManagementUserAPITestCase(APITestCase):
user.refresh_from_db()
self.assertEqual(user.username, TEST_USER_USERNAME_EDITED)
def test_user_edit_remove_groups_via_patch(self):
user = get_user_model().objects.create_user(
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
username=TEST_USER_USERNAME
)
group_1 = Group.objects.create(name='test group 1')
user.groups.add(group_1)
response = self.client.patch(
reverse('rest_api:user-detail', args=(user.pk,)),
)
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertQuerysetEqual(
user.groups.all().order_by('name'), (repr(group_1),)
)
def test_user_edit_add_groups_via_patch(self):
user = get_user_model().objects.create_user(
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
username=TEST_USER_USERNAME
)
group_1 = Group.objects.create(name='test group 1')
response = self.client.patch(
reverse('rest_api:user-detail', args=(user.pk,)),
data={'groups_pk_list': '{}'.format(group_1.pk)}
)
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertQuerysetEqual(
user.groups.all().order_by('name'), (repr(group_1),)
)
def test_user_delete(self):
user = get_user_model().objects.create_user(
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,