Merge pull request #1408 from normanjaeckel/DjangoRESTFramework-Work

Refactored REST api in agenda, core and users app.
This commit is contained in:
Norman Jäckel 2015-01-17 16:27:22 +01:00
commit 413abf2b9a
15 changed files with 134 additions and 60 deletions

View File

@ -16,6 +16,7 @@ Other:
template signals and slides. template signals and slides.
- Used Bower and gulp to manage third party JavaScript and Cascading Style - Used Bower and gulp to manage third party JavaScript and Cascading Style
Sheets libraries. Sheets libraries.
- Added Django REST Framework api.
Version 1.7 (unreleased) Version 1.7 (unreleased)

View File

@ -1,11 +1,11 @@
from rest_framework import serializers from openslides.utils import rest_api
from .models import Item, Speaker from .models import Item, Speaker
class SpeakerSerializer(serializers.HyperlinkedModelSerializer): class SpeakerSerializer(rest_api.serializers.HyperlinkedModelSerializer):
""" """
Serializer for a agenda.models.Speaker objects. Serializer for agenda.models.Speaker objects.
""" """
class Meta: class Meta:
model = Speaker model = Speaker
@ -17,18 +17,17 @@ class SpeakerSerializer(serializers.HyperlinkedModelSerializer):
'weight') 'weight')
class ItemSerializer(serializers.ModelSerializer): class ItemSerializer(rest_api.serializers.HyperlinkedModelSerializer):
""" """
Serializer for a agenda.models.Item objects. Serializer for a agenda.models.Item objects.
""" """
get_title = serializers.CharField(read_only=True) get_title = rest_api.serializers.CharField(read_only=True)
get_title_supplement = serializers.CharField(read_only=True) get_title_supplement = rest_api.serializers.CharField(read_only=True)
item_no = serializers.CharField(read_only=True) item_no = rest_api.serializers.CharField(read_only=True)
speaker_set = SpeakerSerializer(many=True, read_only=True) speaker_set = SpeakerSerializer(many=True, read_only=True)
tags = rest_api.serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='tag-detail')
# content_object = serializers.PrimaryKeyRelatedField(read_only=True) # content_object = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta: class Meta:
model = Item model = Item
exclude = ('content_type', 'object_id') exclude = ('content_type', 'object_id')
# TODO: Problem: User can always see the time shedule. Filter fields with respect of permission.

View File

@ -783,13 +783,12 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to see and in case of create, update or destroy requests permission to see and in case of create, update or destroy requests
the permission to manage. the permission to manage and to see organizational items.
""" """
if not request.user.has_perm('agenda.can_see_agenda'): if (not request.user.has_perm('agenda.can_see_agenda') or
self.permission_denied(request) (self.action in ('create', 'update', 'destroy') and not
elif (self.action in ('create', 'update', 'destroy') (request.user.has_perm('agenda.can_manage_agenda') and
and not request.user.has_perm('agenda.can_manage_agenda')): request.user.has_perm('agenda.can_see_orga_items')))):
# This is the same as self.action not in ('list', 'retrieve')
self.permission_denied(request) self.permission_denied(request)
def check_object_permissions(self, request, obj): def check_object_permissions(self, request, obj):
@ -805,7 +804,6 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet):
Filters organizational items if the user has no permission to see it. Filters organizational items if the user has no permission to see it.
""" """
queryset = Item.objects.all() queryset = Item.objects.all()
if (not self.request.user.has_perm('agenda.can_see_orga_items') and if not self.request.user.has_perm('agenda.can_see_orga_items'):
not self.request.user.has_perm('agenda.can_manage_agenda')):
queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM) queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM)
return queryset return queryset

View File

@ -17,7 +17,7 @@ class CoreAppConfig(AppConfig):
from openslides.utils.autoupdate import inform_changed_data_receiver from openslides.utils.autoupdate import inform_changed_data_receiver
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .signals import setup_general_config from .signals import setup_general_config
from .views import CustomSlideViewSet from .views import CustomSlideViewSet, TagViewSet
# Connect signals. # Connect signals.
config_signal.connect(setup_general_config, dispatch_uid='setup_general_config') config_signal.connect(setup_general_config, dispatch_uid='setup_general_config')
@ -28,6 +28,7 @@ class CoreAppConfig(AppConfig):
# Register viewset. # Register viewset.
router.register('core/customslide', CustomSlideViewSet) router.register('core/customslide', CustomSlideViewSet)
router.register('core/tag', TagViewSet)
# Update data when any model of any installed app is saved or deleted # Update data when any model of any installed app is saved or deleted
signals.post_save.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') signals.post_save.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver')

View File

@ -4,15 +4,16 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
# TODO: activate the following line after using the apploader # TODO: activate the following line after using the apploader
# from django.contrib.auth import get_user_model # from django.contrib.auth import get_user_model
from openslides.utils.models import AbsoluteUrlMixin
from openslides.projector.models import SlideMixin from openslides.projector.models import SlideMixin
from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin
# Imports the default user so that other apps can import it from here. # Imports the default user so that other apps can import it from here.
# TODO: activate this with the new apploader # TODO: activate this with the new apploader
# User = get_user_model() # User = get_user_model()
class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model): class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
""" """
Model for Slides, only for the projector. Model for Slides, only for the projector.
""" """
@ -46,7 +47,7 @@ class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model):
return url return url
class Tag(AbsoluteUrlMixin, models.Model): class Tag(RESTModelMixin, AbsoluteUrlMixin, models.Model):
""" """
Model to save tags. Model to save tags.
""" """

View File

@ -1,11 +1,19 @@
from rest_framework import serializers from openslides.utils import rest_api
from .models import CustomSlide from .models import CustomSlide, Tag
class CustomSlideSerializer(serializers.ModelSerializer): class CustomSlideSerializer(rest_api.serializers.HyperlinkedModelSerializer):
""" """
Serializer for a core.models.CustomSlide objects. Serializer for core.models.CustomSlide objects.
""" """
class Meta: class Meta:
model = CustomSlide model = CustomSlide
class TagSerializer(rest_api.serializers.HyperlinkedModelSerializer):
"""
Serializer for core.models.Tag objects.
"""
class Meta:
model = Tag

View File

@ -21,7 +21,7 @@ from openslides.utils.widgets import Widget
from .forms import SelectWidgetsForm from .forms import SelectWidgetsForm
from .models import CustomSlide, Tag from .models import CustomSlide, Tag
from .exceptions import TagException from .exceptions import TagException
from .serializers import CustomSlideSerializer from .serializers import CustomSlideSerializer, TagSerializer
class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView): class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
@ -217,6 +217,23 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
pass pass
class CustomSlideViewSet(rest_api.viewsets.ModelViewSet):
"""
API endpoint to view, edit and delete custom slides.
"""
model = CustomSlide
queryset = CustomSlide.objects.all()
serializer_class = CustomSlideSerializer
def check_permissions(self, request):
"""
Calls self.permission_denied() if the requesting user has not the
permission to manage.
"""
if not request.user.has_perm('core.can_manage_projector'):
self.permission_denied(request)
class TagListView(utils_views.AjaxMixin, utils_views.ListView): class TagListView(utils_views.AjaxMixin, utils_views.ListView):
""" """
View to list and manipulate tags. View to list and manipulate tags.
@ -295,18 +312,19 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView):
**context) **context)
class CustomSlideViewSet(rest_api.viewsets.ModelViewSet): class TagViewSet(rest_api.viewsets.ModelViewSet):
""" """
API endpoint to view, edit and delete custom slides. API endpoint to view, edit and delete tags.
""" """
model = CustomSlide model = Tag
queryset = CustomSlide.objects.all() queryset = Tag.objects.all()
serializer_class = CustomSlideSerializer serializer_class = TagSerializer
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to manage. permission to manage and it's a create, update or detroy request.
""" """
if not request.user.has_perm('core.can_manage_projector'): if (self.action in ('create', 'update', 'destroy')
and not request.user.has_perm('core.can_manage_tags')):
self.permission_denied(request) self.permission_denied(request)

View File

@ -8,7 +8,7 @@ class UserMainMenuEntry(MainMenuEntry):
Main menu entry for the participant app. Main menu entry for the participant app.
""" """
verbose_name = ugettext_lazy('Users') verbose_name = ugettext_lazy('Users')
required_permission = 'users.can_see' required_permission = 'users.can_see_extra_data'
default_weight = 50 default_weight = 50
pattern_name = 'user_list' pattern_name = 'user_list'
icon_css_class = 'icon-user' icon_css_class = 'icon-user'

View File

@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.projector.models import SlideMixin from openslides.projector.models import SlideMixin
from openslides.utils.models import AbsoluteUrlMixin from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
@ -21,7 +22,7 @@ class UserManager(BaseUserManager):
return user return user
class User(SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser): class User(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser):
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
slide_callback_name = 'user' slide_callback_name = 'user'
@ -72,7 +73,8 @@ class User(SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser):
class Meta: class Meta:
permissions = ( permissions = (
('can_see', ugettext_noop('Can see users')), ('can_see_name', ugettext_noop('Can see names of users')),
('can_see_extra_data', ugettext_noop('Can see extra data of users')),
('can_manage', ugettext_noop('Can manage users')), ('can_manage', ugettext_noop('Can manage users')),
) )
ordering = ('last_name',) ordering = ('last_name',)

View File

@ -1,9 +1,9 @@
from rest_framework import serializers from openslides.utils import rest_api
from .models import User from .models import User
class UserSerializer(serializers.ModelSerializer): class UserShortSerializer(rest_api.serializers.ModelSerializer):
""" """
Serializer for a users.models.User objects. Serializer for a users.models.User objects.
""" """
@ -11,5 +11,26 @@ class UserSerializer(serializers.ModelSerializer):
model = User model = User
fields = ( fields = (
'username', 'username',
'title',
'first_name', 'first_name',
'last_name') 'last_name',
'structure_level')
class UserFullSerializer(rest_api.serializers.ModelSerializer):
"""
Serializer for a users.models.User objects.
"""
class Meta:
model = User
fields = (
'is_present',
'username',
'title',
'first_name',
'last_name',
'structure_level',
'about_me',
'comment',
'default_password',
'is_active')

View File

@ -138,15 +138,27 @@ def create_builtin_groups_and_admin(sender, **kwargs):
perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignment') perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignment')
ct_users = ContentType.objects.get(app_label='users', model='user') ct_users = ContentType.objects.get(app_label='users', model='user')
perm_17 = Permission.objects.get(content_type=ct_users, codename='can_see') perm_users_can_see_name = Permission.objects.get(content_type=ct_users, codename='can_see_name')
perm_users_can_see_extra_data = Permission.objects.get(content_type=ct_users, codename='can_see_extra_data')
ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile') ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile')
perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see') perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see')
base_permission_list = (
perm_11,
perm_12,
perm_13,
perm_14,
perm_15,
perm_16,
perm_users_can_see_name,
perm_users_can_see_extra_data,
perm_18)
group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1) group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1)
group_anonymous.permissions.add(perm_11, perm_12, perm_13, perm_14, perm_15, perm_16, perm_17, perm_18) group_anonymous.permissions.add(*base_permission_list)
group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2) group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2)
group_registered.permissions.add(perm_11, perm_12, perm_13, perm_14, perm_15, perm_16, perm_17, perm_18, can_speak) group_registered.permissions.add(can_speak, *base_permission_list)
# Delegates (pk 3) # Delegates (pk 3)
perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion')
@ -178,8 +190,9 @@ def create_builtin_groups_and_admin(sender, **kwargs):
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35) group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
# add staff permissions # add staff permissions
group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags) group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags)
# add can_see_user permission # add can_see_name and can_see_extra_data permissions
group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system # TODO: Remove this redundancy after cleanup of the permission system.
group_staff.permissions.add(perm_users_can_see_name, perm_users_can_see_extra_data)
# Admin user # Admin user
create_or_reset_admin_user() create_or_reset_admin_user()

View File

@ -20,14 +20,14 @@ from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
UsersettingsForm, UserUpdateForm) UsersettingsForm, UserUpdateForm)
from .models import Group, User from .models import Group, User
from .pdf import users_to_pdf, users_passwords_to_pdf from .pdf import users_to_pdf, users_passwords_to_pdf
from .serializers import UserSerializer from .serializers import UserFullSerializer, UserShortSerializer
class UserListView(ListView): class UserListView(ListView):
""" """
Show all users. Show all users.
""" """
required_permission = 'users.can_see' required_permission = 'users.can_see_extra_data'
context_object_name = 'users' context_object_name = 'users'
def get_queryset(self): def get_queryset(self):
@ -52,7 +52,7 @@ class UserDetailView(DetailView, PermissionMixin):
""" """
Classed based view to show a specific user in the interface. Classed based view to show a specific user in the interface.
""" """
required_permission = 'users.can_see' required_permission = 'users.can_see_extra_data'
model = User model = User
context_object_name = 'shown_user' context_object_name = 'shown_user'
@ -202,7 +202,7 @@ class UsersListPDF(PDFView):
""" """
Generate the userliste as PDF. Generate the userliste as PDF.
""" """
required_permission = 'users.can_see' required_permission = 'users.can_see_extra_data'
filename = ugettext_lazy("user-list") filename = ugettext_lazy("user-list")
document_title = ugettext_lazy('List of Users') document_title = ugettext_lazy('List of Users')
@ -263,21 +263,32 @@ class ResetPasswordView(SingleObjectMixin, QuestionView):
class UserViewSet(rest_api.viewsets.ModelViewSet): class UserViewSet(rest_api.viewsets.ModelViewSet):
""" """
API endpoint to view, edit and delete users. API endpoint to create, view, edit and delete users.
""" """
model = User model = User
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not all
permission to manage. permissions to see users.
""" """
# TODO: More work on this required. if (not request.user.has_perm('users.can_see_name') or
if not request.user.has_perm('users.can_manage'): (self.action in ('create', 'update', 'destroy') and not
(request.user.has_perm('users.can_manage') and
request.user.has_perm('users.can_see_extra_data')))):
self.permission_denied(request) self.permission_denied(request)
def get_serializer_class(self):
"""
Returns different serializer classes with respect to users permissions.
"""
if self.request.user.has_perm('users.can_see_extra_data'):
serializer_class = UserFullSerializer
else:
serializer_class = UserShortSerializer
return serializer_class
class GroupListView(ListView): class GroupListView(ListView):
""" """

View File

@ -1,5 +1,5 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework import permissions, routers, viewsets # noqa from rest_framework import permissions, routers, serializers, viewsets # noqa
router = routers.DefaultRouter() router = routers.DefaultRouter()

View File

@ -55,7 +55,8 @@ class DefaultGroups(TestCase):
'agenda.can_see_orga_items', 'agenda.can_see_orga_items',
'motion.can_see_motion', 'motion.can_see_motion',
'assignment.can_see_assignment', 'assignment.can_see_assignment',
'users.can_see', 'users.can_see_name',
'users.can_see_extra_data',
'mediafile.can_see') 'mediafile.can_see')
for perm_string in default_perms: for perm_string in default_perms:
perm_string_list = [] perm_string_list = []

View File

@ -189,16 +189,16 @@ class LockoutProtection(TestCase):
field=None, field=None,
errors='You can not remove the permission to manage users from the last group you are in.') errors='You can not remove the permission to manage users from the last group you are in.')
def test_remove_permission_can_see_user_from_registered(self): def test_remove_permission_user_can_see_name_from_registered(self):
self.assertTrue(self.user.has_perm('users.can_see')) self.assertTrue(self.user.has_perm('users.can_see_name'))
# Remove perm from registered group # Remove perm from registered group
can_see_perm = Permission.objects.get( can_see_perm = Permission.objects.get(
content_type=ContentType.objects.get(app_label='users', model='user'), content_type=ContentType.objects.get(app_label='users', model='user'),
codename='can_see') codename='can_see_name')
get_registered_group().permissions.remove(can_see_perm) get_registered_group().permissions.remove(can_see_perm)
# Reload user # Reload user
self.user = User.objects.get(pk=1) self.user = User.objects.get(pk=1)
self.assertTrue(self.user.has_perm('users.can_see')) self.assertTrue(self.user.has_perm('users.can_see_name'))
class TestUserSettings(TestCase): class TestUserSettings(TestCase):