diff --git a/CHANGELOG b/CHANGELOG index a7d13ca44..bdefe505d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ Other: template signals and slides. - Used Bower and gulp to manage third party JavaScript and Cascading Style Sheets libraries. +- Added Django REST Framework api. Version 1.7 (unreleased) diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index 576fc8781..d4c7146af 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -1,11 +1,11 @@ -from rest_framework import serializers +from openslides.utils import rest_api 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: model = Speaker @@ -17,18 +17,17 @@ class SpeakerSerializer(serializers.HyperlinkedModelSerializer): 'weight') -class ItemSerializer(serializers.ModelSerializer): +class ItemSerializer(rest_api.serializers.HyperlinkedModelSerializer): """ Serializer for a agenda.models.Item objects. """ - get_title = serializers.CharField(read_only=True) - get_title_supplement = serializers.CharField(read_only=True) - item_no = serializers.CharField(read_only=True) + get_title = rest_api.serializers.CharField(read_only=True) + get_title_supplement = rest_api.serializers.CharField(read_only=True) + item_no = rest_api.serializers.CharField(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) class Meta: model = Item exclude = ('content_type', 'object_id') - - # TODO: Problem: User can always see the time shedule. Filter fields with respect of permission. diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 654cb4dd6..d2eac3696 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -783,13 +783,12 @@ class ItemViewSet(rest_api.viewsets.ModelViewSet): """ Calls self.permission_denied() if the requesting user has not the 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'): - self.permission_denied(request) - elif (self.action in ('create', 'update', 'destroy') - and not request.user.has_perm('agenda.can_manage_agenda')): - # This is the same as self.action not in ('list', 'retrieve') + if (not request.user.has_perm('agenda.can_see_agenda') or + (self.action in ('create', 'update', 'destroy') and not + (request.user.has_perm('agenda.can_manage_agenda') and + request.user.has_perm('agenda.can_see_orga_items')))): self.permission_denied(request) 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. """ queryset = Item.objects.all() - if (not self.request.user.has_perm('agenda.can_see_orga_items') and - not self.request.user.has_perm('agenda.can_manage_agenda')): + if not self.request.user.has_perm('agenda.can_see_orga_items'): queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM) return queryset diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 2f61497e6..65249733c 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -17,7 +17,7 @@ class CoreAppConfig(AppConfig): from openslides.utils.autoupdate import inform_changed_data_receiver from openslides.utils.rest_api import router from .signals import setup_general_config - from .views import CustomSlideViewSet + from .views import CustomSlideViewSet, TagViewSet # Connect signals. config_signal.connect(setup_general_config, dispatch_uid='setup_general_config') @@ -28,6 +28,7 @@ class CoreAppConfig(AppConfig): # Register viewset. router.register('core/customslide', CustomSlideViewSet) + router.register('core/tag', TagViewSet) # 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') diff --git a/openslides/core/models.py b/openslides/core/models.py index 4688609ff..e48070603 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -4,15 +4,16 @@ from django.utils.translation import ugettext_lazy, ugettext_noop # TODO: activate the following line after using the apploader # from django.contrib.auth import get_user_model -from openslides.utils.models import AbsoluteUrlMixin 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. # TODO: activate this with the new apploader # User = get_user_model() -class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model): +class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model): """ Model for Slides, only for the projector. """ @@ -46,7 +47,7 @@ class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model): return url -class Tag(AbsoluteUrlMixin, models.Model): +class Tag(RESTModelMixin, AbsoluteUrlMixin, models.Model): """ Model to save tags. """ diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 1a052dd27..ad8e70f23 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -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: model = CustomSlide + + +class TagSerializer(rest_api.serializers.HyperlinkedModelSerializer): + """ + Serializer for core.models.Tag objects. + """ + class Meta: + model = Tag diff --git a/openslides/core/views.py b/openslides/core/views.py index fade07f12..f0d9cf934 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -21,7 +21,7 @@ from openslides.utils.widgets import Widget from .forms import SelectWidgetsForm from .models import CustomSlide, Tag from .exceptions import TagException -from .serializers import CustomSlideSerializer +from .serializers import CustomSlideSerializer, TagSerializer class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView): @@ -217,6 +217,23 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView): 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): """ View to list and manipulate tags. @@ -295,18 +312,19 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView): **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 - queryset = CustomSlide.objects.all() - serializer_class = CustomSlideSerializer + model = Tag + queryset = Tag.objects.all() + serializer_class = TagSerializer def check_permissions(self, request): """ 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) diff --git a/openslides/users/main_menu.py b/openslides/users/main_menu.py index 18a349d07..5b378d4cc 100644 --- a/openslides/users/main_menu.py +++ b/openslides/users/main_menu.py @@ -8,7 +8,7 @@ class UserMainMenuEntry(MainMenuEntry): Main menu entry for the participant app. """ verbose_name = ugettext_lazy('Users') - required_permission = 'users.can_see' + required_permission = 'users.can_see_extra_data' default_weight = 50 pattern_name = 'user_list' icon_css_class = 'icon-user' diff --git a/openslides/users/models.py b/openslides/users/models.py index 1166a76d8..295f5fa9f 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.projector.models import SlideMixin from openslides.utils.models import AbsoluteUrlMixin +from openslides.utils.rest_api import RESTModelMixin class UserManager(BaseUserManager): @@ -21,7 +22,7 @@ class UserManager(BaseUserManager): return user -class User(SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser): +class User(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser): USERNAME_FIELD = 'username' slide_callback_name = 'user' @@ -72,7 +73,8 @@ class User(SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser): class Meta: 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')), ) ordering = ('last_name',) diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index 96c5fcbd8..c5e61e286 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -1,9 +1,9 @@ -from rest_framework import serializers +from openslides.utils import rest_api from .models import User -class UserSerializer(serializers.ModelSerializer): +class UserShortSerializer(rest_api.serializers.ModelSerializer): """ Serializer for a users.models.User objects. """ @@ -11,5 +11,26 @@ class UserSerializer(serializers.ModelSerializer): model = User fields = ( 'username', + 'title', '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') diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 141086ba9..3089c6f93 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -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') 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') 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.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.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) 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) # 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) - # add can_see_user permission - group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system + # add can_see_name and can_see_extra_data permissions + # 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 create_or_reset_admin_user() diff --git a/openslides/users/views.py b/openslides/users/views.py index 665a26beb..bb12fd376 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -20,14 +20,14 @@ from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm, UsersettingsForm, UserUpdateForm) from .models import Group, User from .pdf import users_to_pdf, users_passwords_to_pdf -from .serializers import UserSerializer +from .serializers import UserFullSerializer, UserShortSerializer class UserListView(ListView): """ Show all users. """ - required_permission = 'users.can_see' + required_permission = 'users.can_see_extra_data' context_object_name = 'users' def get_queryset(self): @@ -52,7 +52,7 @@ class UserDetailView(DetailView, PermissionMixin): """ 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 context_object_name = 'shown_user' @@ -202,7 +202,7 @@ class UsersListPDF(PDFView): """ Generate the userliste as PDF. """ - required_permission = 'users.can_see' + required_permission = 'users.can_see_extra_data' filename = ugettext_lazy("user-list") document_title = ugettext_lazy('List of Users') @@ -263,21 +263,32 @@ class ResetPasswordView(SingleObjectMixin, QuestionView): 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 queryset = User.objects.all() - serializer_class = UserSerializer def check_permissions(self, request): """ - Calls self.permission_denied() if the requesting user has not the - permission to manage. + Calls self.permission_denied() if the requesting user has not all + permissions to see users. """ - # TODO: More work on this required. - if not request.user.has_perm('users.can_manage'): + if (not request.user.has_perm('users.can_see_name') or + (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) + 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): """ diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 52ca3a4f9..c7d01be5c 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,5 +1,5 @@ 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() diff --git a/tests/users/test_models.py b/tests/users/test_models.py index b6b2ef5b2..8ba7f5826 100644 --- a/tests/users/test_models.py +++ b/tests/users/test_models.py @@ -55,7 +55,8 @@ class DefaultGroups(TestCase): 'agenda.can_see_orga_items', 'motion.can_see_motion', 'assignment.can_see_assignment', - 'users.can_see', + 'users.can_see_name', + 'users.can_see_extra_data', 'mediafile.can_see') for perm_string in default_perms: perm_string_list = [] diff --git a/tests/users/test_views.py b/tests/users/test_views.py index 1364ffef1..f59d6d9a7 100644 --- a/tests/users/test_views.py +++ b/tests/users/test_views.py @@ -189,16 +189,16 @@ class LockoutProtection(TestCase): field=None, 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): - self.assertTrue(self.user.has_perm('users.can_see')) + def test_remove_permission_user_can_see_name_from_registered(self): + self.assertTrue(self.user.has_perm('users.can_see_name')) # Remove perm from registered group can_see_perm = Permission.objects.get( 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) # Reload user 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):