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).
|
- Stop loading theme fonts from the web (GitLab #343).
|
||||||
- Add support for attaching multiple tags (GitLab #307).
|
- 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)
|
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
|
:maxdepth: 1
|
||||||
|
|
||||||
2.2
|
2.2
|
||||||
|
2.1.7
|
||||||
2.1.6
|
2.1.6
|
||||||
2.1.5
|
2.1.5
|
||||||
2.1.4
|
2.1.4
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__title__ = 'Mayan EDMS'
|
__title__ = 'Mayan EDMS'
|
||||||
__version__ = '2.1.6'
|
__version__ = '2.1.7'
|
||||||
__build__ = 0x020106
|
__build__ = 0x020107
|
||||||
__author__ = 'Roberto Rosario'
|
__author__ = 'Roberto Rosario'
|
||||||
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
||||||
__description__ = 'Free Open Source Electronic Document Management System'
|
__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 documents.tests import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE
|
||||||
from permissions.models import Role
|
from permissions.models import Role
|
||||||
from permissions.tests.literals import TEST_ROLE_LABEL
|
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
|
from ..models import AccessControlList
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class PermissionTestCase(BaseTestCase):
|
|||||||
self.user = get_user_model().objects.create(
|
self.user = get_user_model().objects.create(
|
||||||
username=TEST_USER_USERNAME
|
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.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||||
|
|
||||||
self.group.user_set.add(self.user)
|
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 mayan.celery import app
|
||||||
from navigation.classes import Separator
|
from navigation.classes import Separator
|
||||||
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .handlers import (
|
from .handlers import (
|
||||||
user_locale_profile_session_config, user_locale_profile_create
|
user_locale_profile_session_config, user_locale_profile_create
|
||||||
@@ -77,6 +78,8 @@ class CommonApp(MayanAppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
super(CommonApp, self).ready()
|
super(CommonApp, self).ready()
|
||||||
|
|
||||||
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
|
||||||
app.conf.CELERYBEAT_SCHEDULE.update(
|
app.conf.CELERYBEAT_SCHEDULE.update(
|
||||||
{
|
{
|
||||||
'task_delete_stale_uploads': {
|
'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.models import Role
|
||||||
from permissions.tests.literals import TEST_ROLE_LABEL
|
from permissions.tests.literals import TEST_ROLE_LABEL
|
||||||
from user_management.tests import (
|
from user_management.tests import (
|
||||||
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, TEST_GROUP,
|
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL,
|
||||||
TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD
|
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseTestCase
|
from .base import BaseTestCase
|
||||||
@@ -33,7 +33,7 @@ class GenericViewTestCase(BaseTestCase):
|
|||||||
password=TEST_USER_PASSWORD
|
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.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||||
self.group.user_set.add(self.user)
|
self.group.user_set.add(self.user)
|
||||||
self.role.groups.add(self.group)
|
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.generic import RedirectView
|
||||||
from django.views.i18n import javascript_catalog, set_language
|
from django.views.i18n import javascript_catalog, set_language
|
||||||
|
|
||||||
|
from api_views import APIContentTypeList
|
||||||
from .views import (
|
from .views import (
|
||||||
AboutView, CurrentUserDetailsView, CurrentUserEditView,
|
AboutView, CurrentUserDetailsView, CurrentUserEditView,
|
||||||
CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView,
|
CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView,
|
||||||
@@ -67,3 +68,10 @@ urlpatterns += [
|
|||||||
r'^set_language/$', set_language, name='set_language'
|
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 mayan.celery import app
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
from rest_api.fields import DynamicSerializerField
|
||||||
from statistics.classes import StatisticNamespace, CharJSLine
|
from statistics.classes import StatisticNamespace, CharJSLine
|
||||||
|
|
||||||
from .handlers import create_default_document_type
|
from .handlers import create_default_document_type
|
||||||
@@ -99,39 +100,9 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
DocumentTypeFilename = self.get_model('DocumentTypeFilename')
|
DocumentTypeFilename = self.get_model('DocumentTypeFilename')
|
||||||
DocumentVersion = self.get_model('DocumentVersion')
|
DocumentVersion = self.get_model('DocumentVersion')
|
||||||
|
|
||||||
DashboardWidget(
|
DynamicSerializerField.add_serializer(
|
||||||
func=new_document_pages_this_month, icon='fa fa-calendar',
|
klass=Document,
|
||||||
label=_('New pages this month'),
|
serializer_class='documents.serializers.DocumentSerializer'
|
||||||
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')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MissingItem(
|
MissingItem(
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'latest_version', 'url', 'uuid', 'versions',
|
'latest_version', 'url', 'uuid', 'versions',
|
||||||
)
|
)
|
||||||
model = Document
|
model = Document
|
||||||
|
read_only_fields = ('document_type',)
|
||||||
|
|
||||||
|
|
||||||
class NewDocumentSerializer(serializers.ModelSerializer):
|
class NewDocumentSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ __all__ = (
|
|||||||
'TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH',
|
'TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH',
|
||||||
'TEST_NON_ASCII_DOCUMENT_FILENAME', 'TEST_NON_ASCII_DOCUMENT_PATH',
|
'TEST_NON_ASCII_DOCUMENT_FILENAME', 'TEST_NON_ASCII_DOCUMENT_PATH',
|
||||||
'TEST_SMALL_DOCUMENT_FILENAME', 'TEST_SMALL_DOCUMENT_PATH',
|
'TEST_SMALL_DOCUMENT_FILENAME', 'TEST_SMALL_DOCUMENT_PATH',
|
||||||
|
'TEST_DOCUMENT_VERSION_COMMENT_EDITED',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filenames
|
# Filenames
|
||||||
TEST_COMPRESSED_DOCUMENTS_FILENAME = 'compressed_documents.zip'
|
TEST_COMPRESSED_DOCUMENTS_FILENAME = 'compressed_documents.zip'
|
||||||
TEST_DEU_DOCUMENT_FILENAME = 'deu_website.png'
|
TEST_DEU_DOCUMENT_FILENAME = 'deu_website.png'
|
||||||
TEST_DOCUMENT_DESCRIPTION = 'test description'
|
TEST_DOCUMENT_DESCRIPTION = 'test description'
|
||||||
|
TEST_DOCUMENT_DESCRIPTION_EDITED = 'test document description edited'
|
||||||
TEST_DOCUMENT_FILENAME = 'mayan_11_1.pdf'
|
TEST_DOCUMENT_FILENAME = 'mayan_11_1.pdf'
|
||||||
TEST_DOCUMENT_TYPE = 'test_document_type'
|
TEST_DOCUMENT_TYPE = 'test_document_type'
|
||||||
TEST_DOCUMENT_TYPE_2 = 'test document type 2'
|
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_HYBRID_DOCUMENT = 'hybrid_text_and_image.pdf'
|
||||||
TEST_MULTI_PAGE_TIFF = 'multi_page.tiff'
|
TEST_MULTI_PAGE_TIFF = 'multi_page.tiff'
|
||||||
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip'
|
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip'
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ from user_management.tests.literals import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE,
|
TEST_DOCUMENT_DESCRIPTION_EDITED, TEST_DOCUMENT_FILENAME,
|
||||||
TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH,
|
TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE,
|
||||||
|
TEST_DOCUMENT_VERSION_COMMENT_EDITED, TEST_SMALL_DOCUMENT_FILENAME,
|
||||||
|
TEST_SMALL_DOCUMENT_PATH
|
||||||
)
|
)
|
||||||
from ..models import Document, DocumentType
|
from ..models import Document, DocumentType
|
||||||
|
|
||||||
@@ -113,6 +115,12 @@ class DocumentAPITestCase(APITestCase):
|
|||||||
self.admin_user.delete()
|
self.admin_user.delete()
|
||||||
self.document_type.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):
|
def test_document_upload(self):
|
||||||
with open(TEST_DOCUMENT_PATH) as file_descriptor:
|
with open(TEST_DOCUMENT_PATH) as file_descriptor:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -293,5 +301,71 @@ class DocumentAPITestCase(APITestCase):
|
|||||||
), mime_type='application/octet-stream; charset=utf-8'
|
), 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):
|
# TODO: def test_document_set_document_type(self):
|
||||||
# pass
|
# 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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common import MayanAppConfig, menu_tools
|
from common import MayanAppConfig, menu_tools
|
||||||
|
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .links import link_events_list
|
from .links import link_events_list
|
||||||
from .licenses import * # NOQA
|
from .licenses import * # NOQA
|
||||||
@@ -21,6 +21,8 @@ class EventsApp(MayanAppConfig):
|
|||||||
super(EventsApp, self).ready()
|
super(EventsApp, self).ready()
|
||||||
Action = apps.get_model(app_label='actstream', model_name='Action')
|
Action = apps.get_model(app_label='actstream', model_name='Action')
|
||||||
|
|
||||||
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=Action, label=_('Timestamp'), attribute='timestamp'
|
source=Action, label=_('Timestamp'), attribute='timestamp'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,26 +7,48 @@ from actstream import action
|
|||||||
|
|
||||||
|
|
||||||
class Event(object):
|
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
|
@classmethod
|
||||||
def get_label(cls, name):
|
def get_label(cls, name):
|
||||||
try:
|
try:
|
||||||
return cls._labels[name]
|
return cls.get(name=name).label
|
||||||
except KeyError:
|
except KeyError as exception:
|
||||||
return _('Unknown or obsolete event type: {0}'.format(name))
|
return unicode(exception)
|
||||||
|
|
||||||
def __init__(self, name, label):
|
def __init__(self, name, label):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.label = label
|
self.label = label
|
||||||
self.event_type = None
|
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):
|
def commit(self, actor=None, action_object=None, target=None):
|
||||||
model = apps.get_model('events', 'EventType')
|
|
||||||
|
|
||||||
if not self.event_type:
|
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
|
name=self.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ class EventType(models.Model):
|
|||||||
max_length=64, unique=True, verbose_name=_('Name')
|
max_length=64, unique=True, verbose_name=_('Name')
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return unicode(Event.get_label(self.name))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Event type')
|
verbose_name = _('Event type')
|
||||||
verbose_name_plural = _('Event types')
|
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 django.conf.urls import url
|
||||||
|
|
||||||
|
from .api_views import (
|
||||||
|
APIEventListView, APIEventTypeListView, APIObjectEventListView
|
||||||
|
)
|
||||||
from .views import EventListView, ObjectEventListView, VerbEventListView
|
from .views import EventListView, ObjectEventListView, VerbEventListView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -15,3 +18,12 @@ urlpatterns = [
|
|||||||
name='events_by_verb'
|
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 MetadataTypeSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('id', 'name', 'label', 'default', 'lookup')
|
fields = (
|
||||||
|
'id', 'name', 'label', 'default', 'lookup', 'parser', 'validation'
|
||||||
|
)
|
||||||
model = MetadataType
|
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_LABEL_2 = 'test metadata type label 2'
|
||||||
TEST_METADATA_TYPE_NAME = 'test'
|
TEST_METADATA_TYPE_NAME = 'test'
|
||||||
TEST_METADATA_TYPE_NAME_2 = 'test metadata type name 2'
|
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_PARSED_VALID_DATE = '2001-01-01'
|
||||||
TEST_VALID_DATE = '2001-1-1'
|
TEST_VALID_DATE = '2001-1-1'
|
||||||
|
|||||||
@@ -16,12 +16,10 @@ from ..models import DocumentMetadata, DocumentTypeMetadataType, MetadataType
|
|||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_LABEL_2,
|
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):
|
class MetadataTypeAPITestCase(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -34,8 +32,10 @@ class MetadataTypeAPITestCase(APITestCase):
|
|||||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def _create_metadata_type(self):
|
||||||
self.admin_user.delete()
|
self.metadata_type = MetadataType.objects.create(
|
||||||
|
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
def test_metadata_type_create(self):
|
def test_metadata_type_create(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@@ -56,26 +56,35 @@ class MetadataTypeAPITestCase(APITestCase):
|
|||||||
self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME)
|
self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME)
|
||||||
|
|
||||||
def test_metadata_type_delete(self):
|
def test_metadata_type_delete(self):
|
||||||
metadata_type = MetadataType.objects.create(
|
self._create_metadata_type()
|
||||||
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.delete(
|
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(response.status_code, 204)
|
||||||
|
|
||||||
self.assertEqual(MetadataType.objects.count(), 0)
|
self.assertEqual(MetadataType.objects.count(), 0)
|
||||||
|
|
||||||
def test_metadata_type_edit(self):
|
def test_metadata_type_detail_view(self):
|
||||||
metadata_type = MetadataType.objects.create(
|
self._create_metadata_type()
|
||||||
label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME
|
|
||||||
|
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(
|
def test_metadata_type_edit_via_patch_view(self):
|
||||||
reverse('rest_api:metadatatype-detail', args=(metadata_type.pk,)),
|
self._create_metadata_type()
|
||||||
data={
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse('rest_api:metadatatype-detail',
|
||||||
|
args=(self.metadata_type.pk,)), data={
|
||||||
'label': TEST_METADATA_TYPE_LABEL_2,
|
'label': TEST_METADATA_TYPE_LABEL_2,
|
||||||
'name': TEST_METADATA_TYPE_NAME_2
|
'name': TEST_METADATA_TYPE_NAME_2
|
||||||
}
|
}
|
||||||
@@ -83,10 +92,37 @@ class MetadataTypeAPITestCase(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
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(self.metadata_type.label, TEST_METADATA_TYPE_LABEL_2)
|
||||||
self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME_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):
|
class DocumentTypeMetadataTypeAPITestCase(APITestCase):
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ from .permissions import (
|
|||||||
permission_role_view
|
permission_role_view
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
PermissionSerializer, RoleNewGroupListSerializer,
|
PermissionSerializer, RoleSerializer, WritableRoleSerializer
|
||||||
RoleNewPermissionSerializer, RoleSerializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,64 +33,12 @@ class APIPermissionList(generics.ListAPIView):
|
|||||||
return super(APIPermissionList, self).get(*args, **kwargs)
|
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):
|
class APIRoleListView(generics.ListCreateAPIView):
|
||||||
serializer_class = RoleSerializer
|
|
||||||
queryset = Role.objects.all()
|
|
||||||
|
|
||||||
permission_classes = (MayanPermission,)
|
|
||||||
filter_backends = (MayanObjectPermissionsFilter,)
|
filter_backends = (MayanObjectPermissionsFilter,)
|
||||||
mayan_object_permissions = {'GET': (permission_role_view,)}
|
mayan_object_permissions = {'GET': (permission_role_view,)}
|
||||||
mayan_view_permissions = {'POST': (permission_role_create,)}
|
mayan_view_permissions = {'POST': (permission_role_create,)}
|
||||||
|
permission_classes = (MayanPermission,)
|
||||||
|
queryset = Role.objects.all()
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -100,6 +47,12 @@ class APIRoleListView(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return super(APIRoleListView, self).get(*args, **kwargs)
|
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):
|
def post(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create a new role.
|
Create a new role.
|
||||||
@@ -157,16 +110,14 @@ class APIRolePermissionList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class APIRoleView(generics.RetrieveUpdateDestroyAPIView):
|
class APIRoleView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
serializer_class = RoleSerializer
|
|
||||||
queryset = Role.objects.all()
|
|
||||||
|
|
||||||
permission_classes = (MayanPermission,)
|
|
||||||
mayan_object_permissions = {
|
mayan_object_permissions = {
|
||||||
'GET': (permission_role_view,),
|
'GET': (permission_role_view,),
|
||||||
'PUT': (permission_role_edit,),
|
'PUT': (permission_role_edit,),
|
||||||
'PATCH': (permission_role_edit,),
|
'PATCH': (permission_role_edit,),
|
||||||
'DELETE': (permission_role_delete,)
|
'DELETE': (permission_role_delete,)
|
||||||
}
|
}
|
||||||
|
permission_classes = (MayanPermission,)
|
||||||
|
queryset = Role.objects.all()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -182,6 +133,12 @@ class APIRoleView(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
return super(APIRoleView, self).get(*args, **kwargs)
|
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):
|
def patch(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Edit the selected role.
|
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.encoding import python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .classes import Permission
|
||||||
from .managers import RoleManager, StoredPermissionManager
|
from .managers import RoleManager, StoredPermissionManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -21,8 +22,6 @@ class StoredPermission(models.Model):
|
|||||||
objects = StoredPermissionManager()
|
objects = StoredPermissionManager()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
from .classes import Permission
|
|
||||||
|
|
||||||
super(StoredPermission, self).__init__(*args, **kwargs)
|
super(StoredPermission, self).__init__(*args, **kwargs)
|
||||||
try:
|
try:
|
||||||
self.volatile_permission = Permission.get(
|
self.volatile_permission = Permission.get(
|
||||||
|
|||||||
@@ -28,50 +28,8 @@ class PermissionSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoleNewGroupListSerializer(serializers.Serializer):
|
class RoleSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
group_pk_list = serializers.CharField(
|
groups = GroupSerializer(many=True)
|
||||||
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.ModelSerializer):
|
class RoleSerializer(serializers.ModelSerializer):
|
||||||
@@ -81,3 +39,77 @@ class RoleSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
fields = ('id', 'label', 'groups', 'permissions')
|
fields = ('id', 'label', 'groups', 'permissions')
|
||||||
model = Role
|
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 django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
from common.tests import BaseTestCase
|
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 ..classes import Permission
|
||||||
from ..models import Role
|
from ..models import Role
|
||||||
@@ -20,7 +20,7 @@ class PermissionTestCase(BaseTestCase):
|
|||||||
self.user = get_user_model().objects.create(
|
self.user = get_user_model().objects.create(
|
||||||
username=TEST_USER_USERNAME
|
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.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||||
|
|
||||||
def test_no_permissions(self):
|
def test_no_permissions(self):
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .api_views import (
|
from .api_views import APIPermissionList, APIRoleListView, APIRoleView
|
||||||
APIPermissionList, APIRoleGroupList, APIRoleListView,
|
|
||||||
APIRolePermissionList, APIRoleView,
|
|
||||||
)
|
|
||||||
from .views import (
|
from .views import (
|
||||||
RoleCreateView, RoleDeleteView, RoleEditView, RoleListView,
|
RoleCreateView, RoleDeleteView, RoleEditView, RoleListView,
|
||||||
SetupRoleMembersView, SetupRolePermissionsView
|
SetupRoleMembersView, SetupRolePermissionsView
|
||||||
@@ -30,16 +27,8 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
|
url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'),
|
||||||
url(r'^roles/$', APIRoleListView.as_view(), name='role-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]+)/$', 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'),
|
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 metadata import MetadataLookup
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
from rest_api.fields import DynamicSerializerField
|
||||||
|
|
||||||
from .links import (
|
from .links import (
|
||||||
link_group_add, link_group_delete, link_group_edit, link_group_list,
|
link_group_add, link_group_delete, link_group_edit, link_group_list,
|
||||||
@@ -48,6 +49,10 @@ class UserManagementApp(MayanAppConfig):
|
|||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
APIEndPoint(app=self, version_string='1')
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
DynamicSerializerField.add_serializer(
|
||||||
|
klass=get_user_model(),
|
||||||
|
serializer_class='user_management.serializers.UserSerializer'
|
||||||
|
)
|
||||||
|
|
||||||
MetadataLookup(
|
MetadataLookup(
|
||||||
description=_('All the groups.'), name='groups',
|
description=_('All the groups.'), name='groups',
|
||||||
|
|||||||
@@ -46,9 +46,13 @@ class UserGroupListSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
groups = GroupSerializer(many=True, read_only=True)
|
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(
|
password = serializers.CharField(
|
||||||
required=False, style={'input_type': 'password'}
|
required=False, style={'input_type': 'password'}, write_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -56,28 +60,45 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'url': {'view_name': 'rest_api:user-detail'}
|
'url': {'view_name': 'rest_api:user-detail'}
|
||||||
}
|
}
|
||||||
fields = (
|
fields = (
|
||||||
'first_name', 'date_joined', 'email', 'groups', 'id', 'is_active',
|
'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list',
|
||||||
'last_login', 'last_name', 'url', 'username', 'password'
|
'id', 'is_active', 'last_login', 'last_name', 'password', 'url',
|
||||||
|
'username'
|
||||||
)
|
)
|
||||||
model = get_user_model()
|
model = get_user_model()
|
||||||
read_only_fields = ('last_login', 'date_joined')
|
read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined')
|
||||||
write_only_fields = ('password',)
|
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):
|
def create(self, validated_data):
|
||||||
validated_data.pop('is_active')
|
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
|
||||||
user = get_user_model().objects.create_user(**validated_data)
|
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):
|
def update(self, instance, validated_data):
|
||||||
|
self.groups_pk_list = validated_data.pop('groups_pk_list', '')
|
||||||
|
|
||||||
if 'password' in validated_data:
|
if 'password' in validated_data:
|
||||||
instance.set_password(validated_data['password'])
|
instance.set_password(validated_data['password'])
|
||||||
validated_data.pop('password')
|
validated_data.pop('password')
|
||||||
|
|
||||||
for attr, value in validated_data.items():
|
instance = super(UserSerializer, self).update(instance, validated_data)
|
||||||
setattr(instance, attr, value)
|
|
||||||
|
|
||||||
instance.save()
|
if self.groups_pk_list:
|
||||||
|
instance.groups.clear()
|
||||||
|
self._add_groups(instance=instance)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,89 @@ class UserManagementUserAPITestCase(APITestCase):
|
|||||||
user = get_user_model().objects.get(pk=response.data['id'])
|
user = get_user_model().objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(user.username, TEST_USER_USERNAME)
|
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):
|
def test_user_edit_via_put(self):
|
||||||
user = get_user_model().objects.create_user(
|
user = get_user_model().objects.create_user(
|
||||||
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
|
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
|
||||||
@@ -79,6 +162,44 @@ class UserManagementUserAPITestCase(APITestCase):
|
|||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
self.assertEqual(user.username, TEST_USER_USERNAME_EDITED)
|
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):
|
def test_user_delete(self):
|
||||||
user = get_user_model().objects.create_user(
|
user = get_user_model().objects.create_user(
|
||||||
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
|
email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,
|
||||||
|
|||||||
Reference in New Issue
Block a user