Merge branch 'master' into master_merge

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

View File

@@ -16,6 +16,14 @@ the user links
- Stop loading theme fonts from the web (GitLab #343). - 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
View File

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

View File

@@ -23,6 +23,7 @@ versions of the documentation contain the release notes for any later releases.
:maxdepth: 1 :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

View File

@@ -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'

View File

@@ -11,7 +11,7 @@ from documents.permissions import permission_document_view
from documents.tests import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from 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)

View File

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

View File

@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.celery import app from 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': {

View File

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

View File

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

View File

@@ -11,8 +11,8 @@ from permissions import Permission
from permissions.models import Role from permissions.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)

View File

@@ -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'
),
]

View File

@@ -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(

View File

@@ -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):

View File

@@ -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'

View File

@@ -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

View File

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

View File

@@ -4,8 +4,8 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from 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'
) )

View File

@@ -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
) )

View File

@@ -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)

View File

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

View File

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

View File

@@ -2,6 +2,9 @@ from __future__ import unicode_literals
from django.conf.urls import url from 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'
),
]

View File

@@ -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

View File

@@ -12,5 +12,7 @@ TEST_METADATA_TYPE_LABEL = 'test metadata type'
TEST_METADATA_TYPE_LABEL_2 = 'test metadata type label 2' TEST_METADATA_TYPE_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'

View File

@@ -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):

View File

@@ -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.

View File

@@ -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(

View File

@@ -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

View File

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

View File

@@ -5,7 +5,7 @@ from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied from 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):

View File

@@ -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'),
] ]

View File

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

View File

@@ -10,6 +10,7 @@ from common.widgets import two_state_template
from metadata import MetadataLookup from 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',

View File

@@ -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

View File

@@ -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,