Merge branch 'master' into master_merge
This commit is contained in:
@@ -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
102
docs/releases/2.1.7.rst
Normal 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/
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
16
mayan/apps/common/api_views.py
Normal file
16
mayan/apps/common/api_views.py
Normal 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')
|
||||
@@ -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': {
|
||||
|
||||
11
mayan/apps/common/serializers.py
Normal file
11
mayan/apps/common/serializers.py
Normal 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
|
||||
11
mayan/apps/common/tests/test_api.py
Normal file
11
mayan/apps/common/tests/test_api.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -131,6 +131,7 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'latest_version', 'url', 'uuid', 'versions',
|
||||
)
|
||||
model = Document
|
||||
read_only_fields = ('document_type',)
|
||||
|
||||
|
||||
class NewDocumentSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
72
mayan/apps/events/api_views.py
Normal file
72
mayan/apps/events/api_views.py
Normal 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
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
45
mayan/apps/events/serializers.py
Normal file
45
mayan/apps/events/serializers.py
Normal 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
|
||||
11
mayan/apps/events/tests/test_api.py
Normal file
11
mayan/apps/events/tests/test_api.py
Normal 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)
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
166
mayan/apps/permissions/tests/test_api.py
Normal file
166
mayan/apps/permissions/tests/test_api.py
Normal 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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
30
mayan/apps/rest_api/fields.py
Normal file
30
mayan/apps/rest_api/fields.py
Normal 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
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user