Merge pull request #1592 from normanjaeckel/CheckPermission
Refactored permission check for REST API viewsets.
This commit is contained in:
commit
66f45ecd1f
@ -20,55 +20,36 @@ from .models import Item, Speaker
|
|||||||
from .serializers import ItemSerializer
|
from .serializers import ItemSerializer
|
||||||
|
|
||||||
|
|
||||||
class AgendaPDF(PDFView):
|
# Viewsets for the REST API
|
||||||
"""
|
|
||||||
Create a full agenda-PDF.
|
|
||||||
"""
|
|
||||||
required_permission = 'agenda.can_see'
|
|
||||||
filename = ugettext_lazy('Agenda')
|
|
||||||
document_title = ugettext_lazy('Agenda')
|
|
||||||
|
|
||||||
def append_to_pdf(self, story):
|
|
||||||
tree = Item.objects.get_tree(only_agenda_items=True, include_content=True)
|
|
||||||
|
|
||||||
def walk_tree(tree, ancestors=0):
|
|
||||||
"""
|
|
||||||
Generator that yields a two-element-tuple. The first element is an
|
|
||||||
agenda-item and the second a number for steps to the root element.
|
|
||||||
"""
|
|
||||||
for element in tree:
|
|
||||||
yield element['item'], ancestors
|
|
||||||
yield from walk_tree(element['children'], ancestors + 1)
|
|
||||||
|
|
||||||
for item, ancestors in walk_tree(tree):
|
|
||||||
if ancestors:
|
|
||||||
space = " " * 6 * ancestors
|
|
||||||
story.append(Paragraph(
|
|
||||||
"%s%s" % (space, escape(item.get_title())),
|
|
||||||
stylesheet['Subitem']))
|
|
||||||
else:
|
|
||||||
story.append(Paragraph(escape(item.get_title()), stylesheet['Item']))
|
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(ModelViewSet):
|
class ItemViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy agenda items.
|
API endpoint for agenda items.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update, destroy, manage_speaker, speak and tree.
|
||||||
"""
|
"""
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see the agenda and in case of create, update or destroy
|
|
||||||
requests the permission to manage the agenda and to see organizational
|
|
||||||
items.
|
|
||||||
"""
|
"""
|
||||||
if (not request.user.has_perm('agenda.can_see') or
|
if self.action in ('list', 'retrieve', 'manage_speaker', 'tree'):
|
||||||
(self.action in ('create', 'update', 'destroy') and not
|
result = self.request.user.has_perm('agenda.can_see')
|
||||||
(request.user.has_perm('agenda.can_manage') and
|
# For manage_speaker and tree requests the rest of the check is
|
||||||
request.user.has_perm('agenda.can_see_orga_items')))):
|
# done in the specific method. See below.
|
||||||
self.permission_denied(request)
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
|
result = (self.request.user.has_perm('agenda.can_see') and
|
||||||
|
self.request.user.has_perm('agenda.can_see_orga_items') and
|
||||||
|
self.request.user.has_perm('agenda.can_manage'))
|
||||||
|
elif self.action == 'speak':
|
||||||
|
result = (self.request.user.has_perm('agenda.can_see') and
|
||||||
|
self.request.user.has_perm('agenda.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
def check_object_permissions(self, request, obj):
|
def check_object_permissions(self, request, obj):
|
||||||
"""
|
"""
|
||||||
@ -97,11 +78,11 @@ class ItemViewSet(ModelViewSet):
|
|||||||
|
|
||||||
Checks also whether the requesting user can do this. He needs at
|
Checks also whether the requesting user can do this. He needs at
|
||||||
least the permissions 'agenda.can_see' (see
|
least the permissions 'agenda.can_see' (see
|
||||||
self.check_permission()). In case of adding himself the permission
|
self.check_view_permissions()). In case of adding himself the
|
||||||
'agenda.can_be_speaker' is required. In case of adding someone else
|
permission 'agenda.can_be_speaker' is required. In case of adding
|
||||||
the permission 'agenda.can_manage' is required. In case of removing
|
someone else the permission 'agenda.can_manage' is required. In
|
||||||
someone else 'agenda.can_manage' is required. In case of removing
|
case of removing someone else 'agenda.can_manage' is required. In
|
||||||
himself no other permission is required.
|
case of removing himself no other permission is required.
|
||||||
"""
|
"""
|
||||||
# Retrieve item.
|
# Retrieve item.
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
@ -174,16 +155,7 @@ class ItemViewSet(ModelViewSet):
|
|||||||
Special view endpoint to begin and end speach of speakers. Send PUT
|
Special view endpoint to begin and end speach of speakers. Send PUT
|
||||||
{'speaker': <speaker_id>} to begin speach. Omit data to begin speach of
|
{'speaker': <speaker_id>} to begin speach. Omit data to begin speach of
|
||||||
the next speaker. Send DELETE to end speach of current speaker.
|
the next speaker. Send DELETE to end speach of current speaker.
|
||||||
|
|
||||||
Checks also whether the requesting user can do this. He needs at
|
|
||||||
least the permissions 'agenda.can_see' (see
|
|
||||||
self.check_permission()). Also the permission 'agenda.can_manage'
|
|
||||||
is required.
|
|
||||||
"""
|
"""
|
||||||
# Check permission.
|
|
||||||
if not self.request.user.has_perm('agenda.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Retrieve item.
|
# Retrieve item.
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
|
||||||
@ -234,3 +206,35 @@ class ItemViewSet(ModelViewSet):
|
|||||||
else:
|
else:
|
||||||
return Response({'detail': 'Agenda tree successfully updated.'})
|
return Response({'detail': 'Agenda tree successfully updated.'})
|
||||||
return Response(Item.objects.get_tree())
|
return Response(Item.objects.get_tree())
|
||||||
|
|
||||||
|
|
||||||
|
# Views to generate PDFs
|
||||||
|
|
||||||
|
class AgendaPDF(PDFView):
|
||||||
|
"""
|
||||||
|
Create a full agenda-PDF.
|
||||||
|
"""
|
||||||
|
required_permission = 'agenda.can_see'
|
||||||
|
filename = ugettext_lazy('Agenda')
|
||||||
|
document_title = ugettext_lazy('Agenda')
|
||||||
|
|
||||||
|
def append_to_pdf(self, story):
|
||||||
|
tree = Item.objects.get_tree(only_agenda_items=True, include_content=True)
|
||||||
|
|
||||||
|
def walk_tree(tree, ancestors=0):
|
||||||
|
"""
|
||||||
|
Generator that yields a two-element-tuple. The first element is an
|
||||||
|
agenda-item and the second a number for steps to the root element.
|
||||||
|
"""
|
||||||
|
for element in tree:
|
||||||
|
yield element['item'], ancestors
|
||||||
|
yield from walk_tree(element['children'], ancestors + 1)
|
||||||
|
|
||||||
|
for item, ancestors in walk_tree(tree):
|
||||||
|
if ancestors:
|
||||||
|
space = " " * 6 * ancestors
|
||||||
|
story.append(Paragraph(
|
||||||
|
"%s%s" % (space, escape(item.get_title())),
|
||||||
|
stylesheet['Subitem']))
|
||||||
|
else:
|
||||||
|
story.append(Paragraph(escape(item.get_title()), stylesheet['Item']))
|
||||||
|
@ -37,24 +37,37 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Viewsets for the REST API
|
||||||
|
|
||||||
class AssignmentViewSet(ModelViewSet):
|
class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy assignments
|
API endpoint for assignments.
|
||||||
and to manage candidatures.
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update, destroy, candidature_self, candidature_other, mark_elected and
|
||||||
|
create_poll.
|
||||||
"""
|
"""
|
||||||
queryset = Assignment.objects.all()
|
queryset = Assignment.objects.all()
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see assignments and in case of create, update,
|
|
||||||
partial_update or destroy requests the permission to manage
|
|
||||||
assignments.
|
|
||||||
"""
|
"""
|
||||||
if (not request.user.has_perm('assignments.can_see') or
|
if self.action in ('list', 'retrieve'):
|
||||||
(self.action in ('create', 'update', 'partial_update', 'destroy') and
|
result = self.request.user.has_perm('assignments.can_see')
|
||||||
not request.user.has_perm('assignments.can_manage'))):
|
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
||||||
self.permission_denied(request)
|
'mark_elected', 'create_poll'):
|
||||||
|
result = (self.request.user.has_perm('assignments.can_see') and
|
||||||
|
self.request.user.has_perm('assignments.can_manage'))
|
||||||
|
elif self.action == 'candidature_self':
|
||||||
|
result = (self.request.user.has_perm('assignments.can_see') and
|
||||||
|
self.request.user.has_perm('assignments.can_nominate_self'))
|
||||||
|
elif self.action == 'candidature_other':
|
||||||
|
result = (self.request.user.has_perm('assignments.can_see') and
|
||||||
|
self.request.user.has_perm('assignments.can_nominate_other'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""
|
"""
|
||||||
@ -72,8 +85,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
View to nominate self as candidate (POST) or withdraw own
|
View to nominate self as candidate (POST) or withdraw own
|
||||||
candidature (DELETE).
|
candidature (DELETE).
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('assignments.can_nominate_self'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if assignment.is_elected(request.user):
|
if assignment.is_elected(request.user):
|
||||||
raise ValidationError({'detail': _('You are already elected.')})
|
raise ValidationError({'detail': _('You are already elected.')})
|
||||||
@ -134,8 +145,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
View to nominate other users (POST) or delete their candidature
|
View to nominate other users (POST) or delete their candidature
|
||||||
status (DELETE). The client has to send {'user': <id>}.
|
status (DELETE). The client has to send {'user': <id>}.
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('assignments.can_nominate_other'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
user = self.get_user_from_request_data(request)
|
user = self.get_user_from_request_data(request)
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if assignment.is_elected(user):
|
if assignment.is_elected(user):
|
||||||
@ -181,8 +190,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
View to mark other users as elected (POST) or undo this (DELETE).
|
View to mark other users as elected (POST) or undo this (DELETE).
|
||||||
The client has to send {'user': <id>}.
|
The client has to send {'user': <id>}.
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('assignments.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
user = self.get_user_from_request_data(request)
|
user = self.get_user_from_request_data(request)
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -204,8 +211,6 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
View to create a poll. It is a POST request without any data.
|
View to create a poll. It is a POST request without any data.
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('assignments.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if not assignment.candidates.exists():
|
if not assignment.candidates.exists():
|
||||||
raise ValidationError({'detail': _('Can not create poll because there are no candidates.')})
|
raise ValidationError({'detail': _('Can not create poll because there are no candidates.')})
|
||||||
@ -216,21 +221,23 @@ class AssignmentViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to update and destroy assignment polls.
|
API endpoint for assignment polls.
|
||||||
|
|
||||||
|
There are the following views: update and destroy.
|
||||||
"""
|
"""
|
||||||
queryset = AssignmentPoll.objects.all()
|
queryset = AssignmentPoll.objects.all()
|
||||||
serializer_class = AssignmentAllPollSerializer
|
serializer_class = AssignmentAllPollSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see assignments and to manage assignments.
|
|
||||||
"""
|
"""
|
||||||
if (not request.user.has_perm('assignments.can_see') or
|
return (self.request.user.has_perm('assignments.can_see') and
|
||||||
not request.user.has_perm('assignments.can_manage')):
|
self.request.user.has_perm('assignments.can_manage'))
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
|
|
||||||
|
# Views to generate PDFs
|
||||||
|
|
||||||
class AssignmentPDF(PDFView):
|
class AssignmentPDF(PDFView):
|
||||||
required_permission = 'assignments.can_see'
|
required_permission = 'assignments.can_see'
|
||||||
top_space = 0
|
top_space = 0
|
||||||
|
@ -35,6 +35,8 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Special Django views
|
||||||
|
|
||||||
class IndexView(utils_views.CSRFMixin, utils_views.View):
|
class IndexView(utils_views.CSRFMixin, utils_views.View):
|
||||||
"""
|
"""
|
||||||
The primary view for OpenSlides using AngularJS.
|
The primary view for OpenSlides using AngularJS.
|
||||||
@ -61,28 +63,74 @@ class ProjectorView(utils_views.View):
|
|||||||
return HttpResponse(content)
|
return HttpResponse(content)
|
||||||
|
|
||||||
|
|
||||||
|
class AppsJsView(utils_views.View):
|
||||||
|
"""
|
||||||
|
Returns javascript code to be called in the angular app.
|
||||||
|
|
||||||
|
The javascript code loads all js-files defined by the installed (django)
|
||||||
|
apps and creates the angular modules for each angular app.
|
||||||
|
"""
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
angular_modules = []
|
||||||
|
js_files = []
|
||||||
|
for app_config in apps.get_app_configs():
|
||||||
|
# Add the angular app, if the module has one.
|
||||||
|
if getattr(app_config,
|
||||||
|
'angular_{}_module'.format(kwargs.get('openslides_app')),
|
||||||
|
False):
|
||||||
|
angular_modules.append('OpenSlidesApp.{app_name}.{app}'.format(
|
||||||
|
app=kwargs.get('openslides_app'),
|
||||||
|
app_name=app_config.label))
|
||||||
|
|
||||||
|
# Add all js files that the module needs
|
||||||
|
try:
|
||||||
|
app_js_files = app_config.js_files
|
||||||
|
except AttributeError:
|
||||||
|
# The app needs no js-files
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
js_files += [
|
||||||
|
'{static}{path}'.format(
|
||||||
|
static=settings.STATIC_URL,
|
||||||
|
path=path)
|
||||||
|
for path in app_js_files]
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
"angular.module('OpenSlidesApp.{app}', {angular_modules});"
|
||||||
|
"var deferres = [];"
|
||||||
|
"{js_files}.forEach(function(js_file)deferres.push($.getScript(js_file)));"
|
||||||
|
"$.when.apply(this, deferres).done(function() angular.bootstrap(document,['OpenSlidesApp.{app}']));"
|
||||||
|
.format(
|
||||||
|
app=kwargs.get('openslides_app'),
|
||||||
|
angular_modules=angular_modules,
|
||||||
|
js_files=js_files))
|
||||||
|
|
||||||
|
|
||||||
|
# Viewsets for the REST API
|
||||||
|
|
||||||
class ProjectorViewSet(ReadOnlyModelViewSet):
|
class ProjectorViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve and update the projector slide info.
|
API endpoint for the projector slide info.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, activate_elements,
|
||||||
|
prune_elements, deactivate_elements and clear_elements
|
||||||
"""
|
"""
|
||||||
queryset = Projector.objects.all()
|
queryset = Projector.objects.all()
|
||||||
serializer_class = ProjectorSerializer
|
serializer_class = ProjectorSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see the projector and in case of an update request the
|
|
||||||
permission to manage the projector.
|
|
||||||
"""
|
"""
|
||||||
manage_methods = (
|
if self.action in ('list', 'retrieve'):
|
||||||
'activate_elements',
|
result = self.request.user.has_perm('core.can_see_projector')
|
||||||
'prune_elements',
|
elif self.action in ('activate_elements', 'prune_elements',
|
||||||
'deactivate_elements',
|
'deactivate_elements', 'clear_elements'):
|
||||||
'clear_elements')
|
result = (self.request.user.has_perm('core.can_see_projector') and
|
||||||
if (not request.user.has_perm('core.can_see_projector') or
|
self.request.user.has_perm('core.can_manage_projector'))
|
||||||
(self.action in manage_methods and
|
else:
|
||||||
not request.user.has_perm('core.can_manage_projector'))):
|
result = False
|
||||||
self.permission_denied(request)
|
return result
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['post'])
|
||||||
def activate_elements(self, request, pk):
|
def activate_elements(self, request, pk):
|
||||||
@ -168,38 +216,135 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
class CustomSlideViewSet(ModelViewSet):
|
class CustomSlideViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy custom slides.
|
API endpoint for custom slides.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
"""
|
"""
|
||||||
queryset = CustomSlide.objects.all()
|
queryset = CustomSlide.objects.all()
|
||||||
serializer_class = CustomSlideSerializer
|
serializer_class = CustomSlideSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to manage projector.
|
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('core.can_manage_projector'):
|
return self.request.user.has_perm('core.can_manage_projector')
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy tags.
|
API endpoint for tags.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
"""
|
"""
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all()
|
||||||
serializer_class = TagSerializer
|
serializer_class = TagSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to manage tags and it is a create, update or detroy request.
|
|
||||||
Users without permissions are able to list and retrieve tags.
|
|
||||||
"""
|
"""
|
||||||
if (self.action in ('create', 'update', 'destroy') and
|
if self.action in ('list', 'retrieve'):
|
||||||
not request.user.has_perm('core.can_manage_tags')):
|
# Every authenticated user can list or retrieve tags.
|
||||||
self.permission_denied(request)
|
# Anonymous users can do so if they are enabled.
|
||||||
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
|
elif self.action in ('create', 'update', 'destroy'):
|
||||||
|
result = self.request.user.has_perm('core.can_manage_tags')
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigMetadata(SimpleMetadata):
|
||||||
|
"""
|
||||||
|
Custom metadata class to add config info to responses on OPTIONS requests.
|
||||||
|
"""
|
||||||
|
def determine_metadata(self, request, view):
|
||||||
|
# Sort config variables by weight.
|
||||||
|
config_variables = sorted(config.get_config_variables().values(), key=attrgetter('weight'))
|
||||||
|
|
||||||
|
# Build tree.
|
||||||
|
config_groups = []
|
||||||
|
for config_variable in config_variables:
|
||||||
|
if not config_groups or config_groups[-1]['name'] != config_variable.group:
|
||||||
|
config_groups.append(OrderedDict(
|
||||||
|
name=config_variable.group,
|
||||||
|
subgroups=[]))
|
||||||
|
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup:
|
||||||
|
config_groups[-1]['subgroups'].append(OrderedDict(
|
||||||
|
name=config_variable.subgroup,
|
||||||
|
items=[]))
|
||||||
|
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data)
|
||||||
|
|
||||||
|
# Add tree to metadata.
|
||||||
|
metadata = super().determine_metadata(request, view)
|
||||||
|
metadata['config_groups'] = config_groups
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigViewSet(ViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for the config.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve and update.
|
||||||
|
"""
|
||||||
|
metadata_class = ConfigMetadata
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
# Every authenticated user can list or retrieve the config.
|
||||||
|
# Anonymous users can do so if they are enabled.
|
||||||
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
|
elif self.action == 'update':
|
||||||
|
result = self.request.user.has_perm('core.can_manage_config')
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
"""
|
||||||
|
Lists all config variables. Everybody can see them.
|
||||||
|
"""
|
||||||
|
return Response([{'key': key, 'value': value} for key, value in config.items()])
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Retrieves a config variable. Everybody can see it.
|
||||||
|
"""
|
||||||
|
key = kwargs['pk']
|
||||||
|
try:
|
||||||
|
value = config[key]
|
||||||
|
except ConfigNotFound:
|
||||||
|
raise Http404
|
||||||
|
return Response({'key': key, 'value': value})
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Updates a config variable. Only managers can do this.
|
||||||
|
|
||||||
|
Example: {"value": 42}
|
||||||
|
"""
|
||||||
|
key = kwargs['pk']
|
||||||
|
value = request.data['value']
|
||||||
|
|
||||||
|
# Validate and change value.
|
||||||
|
try:
|
||||||
|
config[key] = value
|
||||||
|
except ConfigNotFound:
|
||||||
|
raise Http404
|
||||||
|
except ConfigError as e:
|
||||||
|
raise ValidationError({'detail': e})
|
||||||
|
|
||||||
|
# Return response.
|
||||||
|
return Response({'key': key, 'value': value})
|
||||||
|
|
||||||
|
|
||||||
|
# Special API views
|
||||||
|
|
||||||
class UrlPatternsView(utils_views.APIView):
|
class UrlPatternsView(utils_views.APIView):
|
||||||
"""
|
"""
|
||||||
Returns a dictionary with all url patterns as json. The patterns kwargs
|
Returns a dictionary with all url patterns as json. The patterns kwargs
|
||||||
@ -234,121 +379,3 @@ class VersionView(utils_views.APIView):
|
|||||||
'description': get_plugin_description(plugin),
|
'description': get_plugin_description(plugin),
|
||||||
'version': get_plugin_version(plugin)})
|
'version': get_plugin_version(plugin)})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ConfigMetadata(SimpleMetadata):
|
|
||||||
"""
|
|
||||||
Custom metadata class to add config info to responses on OPTIONS requests.
|
|
||||||
"""
|
|
||||||
def determine_metadata(self, request, view):
|
|
||||||
# Sort config variables by weight.
|
|
||||||
config_variables = sorted(config.get_config_variables().values(), key=attrgetter('weight'))
|
|
||||||
|
|
||||||
# Build tree.
|
|
||||||
config_groups = []
|
|
||||||
for config_variable in config_variables:
|
|
||||||
if not config_groups or config_groups[-1]['name'] != config_variable.group:
|
|
||||||
config_groups.append(OrderedDict(
|
|
||||||
name=config_variable.group,
|
|
||||||
subgroups=[]))
|
|
||||||
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup:
|
|
||||||
config_groups[-1]['subgroups'].append(OrderedDict(
|
|
||||||
name=config_variable.subgroup,
|
|
||||||
items=[]))
|
|
||||||
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data)
|
|
||||||
|
|
||||||
# Add tree to metadata.
|
|
||||||
metadata = super().determine_metadata(request, view)
|
|
||||||
metadata['config_groups'] = config_groups
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigViewSet(ViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve and update the config.
|
|
||||||
"""
|
|
||||||
metadata_class = ConfigMetadata
|
|
||||||
|
|
||||||
def list(self, request):
|
|
||||||
"""
|
|
||||||
Lists all config variables. Everybody can see them.
|
|
||||||
"""
|
|
||||||
return Response([{'key': key, 'value': value} for key, value in config.items()])
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Retrieves a config variable. Everybody can see it.
|
|
||||||
"""
|
|
||||||
key = kwargs['pk']
|
|
||||||
try:
|
|
||||||
value = config[key]
|
|
||||||
except ConfigNotFound:
|
|
||||||
raise Http404
|
|
||||||
return Response({'key': key, 'value': value})
|
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Updates a config variable. Only managers can do this.
|
|
||||||
|
|
||||||
Example: {"value": 42}
|
|
||||||
"""
|
|
||||||
# Check permission.
|
|
||||||
if not request.user.has_perm('core.can_manage_config'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
key = kwargs['pk']
|
|
||||||
value = request.data['value']
|
|
||||||
|
|
||||||
# Validate and change value.
|
|
||||||
try:
|
|
||||||
config[key] = value
|
|
||||||
except ConfigNotFound:
|
|
||||||
raise Http404
|
|
||||||
except ConfigError as e:
|
|
||||||
raise ValidationError({'detail': e})
|
|
||||||
|
|
||||||
# Return response.
|
|
||||||
return Response({'key': key, 'value': value})
|
|
||||||
|
|
||||||
|
|
||||||
class AppsJsView(utils_views.View):
|
|
||||||
"""
|
|
||||||
Returns javascript code to be called in the angular app.
|
|
||||||
|
|
||||||
The javascript code loads all js-files defined by the installed (django)
|
|
||||||
apps and creates the angular modules for each angular app.
|
|
||||||
"""
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
angular_modules = []
|
|
||||||
js_files = []
|
|
||||||
for app_config in apps.get_app_configs():
|
|
||||||
# Add the angular app, if the module has one.
|
|
||||||
if getattr(app_config,
|
|
||||||
'angular_{}_module'.format(kwargs.get('openslides_app')),
|
|
||||||
False):
|
|
||||||
angular_modules.append('OpenSlidesApp.{app_name}.{app}'.format(
|
|
||||||
app=kwargs.get('openslides_app'),
|
|
||||||
app_name=app_config.label))
|
|
||||||
|
|
||||||
# Add all js files that the module needs
|
|
||||||
try:
|
|
||||||
app_js_files = app_config.js_files
|
|
||||||
except AttributeError:
|
|
||||||
# The app needs no js-files
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
js_files += [
|
|
||||||
'{static}{path}'.format(
|
|
||||||
static=settings.STATIC_URL,
|
|
||||||
path=path)
|
|
||||||
for path in app_js_files]
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
"angular.module('OpenSlidesApp.{app}', {angular_modules});"
|
|
||||||
"var deferres = [];"
|
|
||||||
"{js_files}.forEach(function(js_file)deferres.push($.getScript(js_file)));"
|
|
||||||
"$.when.apply(this, deferres).done(function() angular.bootstrap(document,['OpenSlidesApp.{app}']));"
|
|
||||||
.format(
|
|
||||||
app=kwargs.get('openslides_app'),
|
|
||||||
angular_modules=angular_modules,
|
|
||||||
js_files=js_files))
|
|
||||||
|
@ -4,23 +4,33 @@ from .models import Mediafile
|
|||||||
from .serializers import MediafileSerializer
|
from .serializers import MediafileSerializer
|
||||||
|
|
||||||
|
|
||||||
|
# Viewsets for the REST API
|
||||||
|
|
||||||
class MediafileViewSet(ModelViewSet):
|
class MediafileViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy mediafile
|
API endpoint for mediafile objects.
|
||||||
objects.
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
"""
|
"""
|
||||||
queryset = Mediafile.objects.all()
|
queryset = Mediafile.objects.all()
|
||||||
serializer_class = MediafileSerializer
|
serializer_class = MediafileSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see mediafile objects and in case of create, update or
|
|
||||||
destroy requests the permission to manage mediafile objects.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Use mediafiles.can_upload permission to create and update some
|
# TODO: Use mediafiles.can_upload permission to create and update some
|
||||||
# objects but restricted concerning the uploader.
|
# objects but restricted concerning the uploader.
|
||||||
if (not request.user.has_perm('mediafiles.can_see') or
|
if self.action in ('list', 'retrieve'):
|
||||||
(self.action in ('create', 'update', 'destroy') and not
|
result = self.request.user.has_perm('mediafiles.can_see')
|
||||||
request.user.has_perm('mediafiles.can_manage'))):
|
elif self.action in ('create', 'partial_update', 'update'):
|
||||||
self.permission_denied(request)
|
result = (self.request.user.has_perm('mediafiles.can_see') and
|
||||||
|
self.request.user.has_perm('mediafiles.can_upload') and
|
||||||
|
self.request.user.has_perm('mediafiles.can_manage'))
|
||||||
|
elif self.action == 'destroy':
|
||||||
|
result = (self.request.user.has_perm('mediafiles.can_see') and
|
||||||
|
self.request.user.has_perm('mediafiles.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
@ -24,40 +24,45 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Viewsets for the REST API
|
||||||
|
|
||||||
class MotionViewSet(ModelViewSet):
|
class MotionViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint to list, retrieve, create, update and destroy motions.
|
API endpoint for motions.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update, destroy, manage_version, support and set_state.
|
||||||
"""
|
"""
|
||||||
queryset = Motion.objects.all()
|
queryset = Motion.objects.all()
|
||||||
serializer_class = MotionSerializer
|
serializer_class = MotionSerializer
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
Returns True if the user has required permissions.
|
||||||
permission to see motions and in case of destroy requests the
|
|
||||||
permission to manage motions.
|
|
||||||
"""
|
"""
|
||||||
if (not request.user.has_perm('motions.can_see') or
|
if self.action in ('list', 'retrieve', 'partial_update', 'update'):
|
||||||
(self.action == 'destroy' and not request.user.has_perm('motions.can_manage'))):
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
self.permission_denied(request)
|
# For partial_update and update requests the rest of the check is
|
||||||
|
# done in the update method. See below.
|
||||||
|
elif self.action == 'create':
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_create') and
|
||||||
|
(not config['motions_stop_submitting'] or
|
||||||
|
self.request.user.has_perm('motions.can_manage')))
|
||||||
|
elif self.action in ('destroy', 'manage_version', 'set_state'):
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_manage'))
|
||||||
|
elif self.action == 'support':
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_support'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Customized view endpoint to create a new motion.
|
Customized view endpoint to create a new motion.
|
||||||
|
|
||||||
Checks also whether the requesting user can submit a new motion. He
|
|
||||||
needs at least the permissions 'motions.can_see' (see
|
|
||||||
self.check_permission()) and 'motions.can_create'. If the
|
|
||||||
submitting of new motions by non-staff users is stopped via config
|
|
||||||
variable 'motions_stop_submitting', the requesting user needs also
|
|
||||||
to have the permission 'motions.can_manage'.
|
|
||||||
"""
|
"""
|
||||||
# Check permissions.
|
|
||||||
if (not request.user.has_perm('motions.can_create') or
|
|
||||||
(not config['motions_stop_submitting'] and
|
|
||||||
not request.user.has_perm('motions.can_manage'))):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Check permission to send submitter and supporter data.
|
# Check permission to send submitter and supporter data.
|
||||||
if (not request.user.has_perm('motions.can_manage') and
|
if (not request.user.has_perm('motions.can_manage') and
|
||||||
(request.data.getlist('submitters') or request.data.getlist('supporters'))):
|
(request.data.getlist('submitters') or request.data.getlist('supporters'))):
|
||||||
@ -80,7 +85,7 @@ class MotionViewSet(ModelViewSet):
|
|||||||
|
|
||||||
Checks also whether the requesting user can update the motion. He
|
Checks also whether the requesting user can update the motion. He
|
||||||
needs at least the permissions 'motions.can_see' (see
|
needs at least the permissions 'motions.can_see' (see
|
||||||
self.check_permission()). Also the instance method
|
self.check_view_permissions()). Also the instance method
|
||||||
get_allowed_actions() is evaluated.
|
get_allowed_actions() is evaluated.
|
||||||
"""
|
"""
|
||||||
# Get motion.
|
# Get motion.
|
||||||
@ -122,10 +127,6 @@ class MotionViewSet(ModelViewSet):
|
|||||||
{'version_number': <number>} to delete a version. Deleting the
|
{'version_number': <number>} to delete a version. Deleting the
|
||||||
active version is not allowed. Only managers can use this view.
|
active version is not allowed. Only managers can use this view.
|
||||||
"""
|
"""
|
||||||
# Check permission.
|
|
||||||
if not request.user.has_perm('motions.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Retrieve motion and version.
|
# Retrieve motion and version.
|
||||||
motion = self.get_object()
|
motion = self.get_object()
|
||||||
version_number = request.data.get('version_number')
|
version_number = request.data.get('version_number')
|
||||||
@ -168,17 +169,7 @@ class MotionViewSet(ModelViewSet):
|
|||||||
(unsupport).
|
(unsupport).
|
||||||
|
|
||||||
Send POST to support and DELETE to unsupport.
|
Send POST to support and DELETE to unsupport.
|
||||||
|
|
||||||
Checks also whether the requesting user can do this. He needs at
|
|
||||||
least the permissions 'motions.can_see' (see
|
|
||||||
self.check_permission()). Also the the permission
|
|
||||||
'motions.can_support' is required and the instance method
|
|
||||||
get_allowed_actions() is evaluated.
|
|
||||||
"""
|
"""
|
||||||
# Check permission.
|
|
||||||
if not request.user.has_perm('motions.can_support'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Retrieve motion and allowed actions.
|
# Retrieve motion and allowed actions.
|
||||||
motion = self.get_object()
|
motion = self.get_object()
|
||||||
allowed_actions = motion.get_allowed_actions(request.user)
|
allowed_actions = motion.get_allowed_actions(request.user)
|
||||||
@ -211,10 +202,6 @@ class MotionViewSet(ModelViewSet):
|
|||||||
Send PUT {'state': <state_id>} to set and just PUT {} to reset the
|
Send PUT {'state': <state_id>} to set and just PUT {} to reset the
|
||||||
state. Only managers can use this view.
|
state. Only managers can use this view.
|
||||||
"""
|
"""
|
||||||
# Check permission.
|
|
||||||
if not request.user.has_perm('motions.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Retrieve motion and state.
|
# Retrieve motion and state.
|
||||||
motion = self.get_object()
|
motion = self.get_object()
|
||||||
state = request.data.get('state')
|
state = request.data.get('state')
|
||||||
@ -245,6 +232,56 @@ class MotionViewSet(ModelViewSet):
|
|||||||
return Response({'detail': message})
|
return Response({'detail': message})
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for categories.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
|
"""
|
||||||
|
queryset = Category.objects.all()
|
||||||
|
serializer_class = CategorySerializer
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for workflows.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
|
"""
|
||||||
|
queryset = Workflow.objects.all()
|
||||||
|
serializer_class = WorkflowSerializer
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Views to generate PDFs
|
||||||
|
|
||||||
class PollPDFView(PDFView):
|
class PollPDFView(PDFView):
|
||||||
"""
|
"""
|
||||||
Generates a ballotpaper.
|
Generates a ballotpaper.
|
||||||
@ -349,41 +386,3 @@ class MotionPDFView(SingleObjectMixin, PDFView):
|
|||||||
motions_to_pdf(pdf, motions)
|
motions_to_pdf(pdf, motions)
|
||||||
else:
|
else:
|
||||||
motion_to_pdf(pdf, self.get_object())
|
motion_to_pdf(pdf, self.get_object())
|
||||||
|
|
||||||
|
|
||||||
class CategoryViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and destroy categories.
|
|
||||||
"""
|
|
||||||
queryset = Category.objects.all()
|
|
||||||
serializer_class = CategorySerializer
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to see motions and in case of create, update or destroy
|
|
||||||
requests the permission to manage motions.
|
|
||||||
"""
|
|
||||||
if (not request.user.has_perm('motions.can_see') or
|
|
||||||
(self.action in ('create', 'update', 'destroy') and not
|
|
||||||
request.user.has_perm('motions.can_manage'))):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and destroy workflows.
|
|
||||||
"""
|
|
||||||
queryset = Workflow.objects.all()
|
|
||||||
serializer_class = WorkflowSerializer
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to see motions and in case of create, update or destroy
|
|
||||||
requests the permission to manage motions.
|
|
||||||
"""
|
|
||||||
if (not request.user.has_perm('motions.can_see') or
|
|
||||||
(self.action in ('create', 'update', 'destroy') and not
|
|
||||||
request.user.has_perm('motions.can_manage'))):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
@ -18,6 +18,149 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Viewsets for the REST API
|
||||||
|
|
||||||
|
class UserViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for users.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update, destroy and reset_password.
|
||||||
|
"""
|
||||||
|
queryset = User.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.request.user.has_perm('users.can_see_name')
|
||||||
|
elif self.action in ('create', 'partial_update', 'update', 'destroy', 'reset_password'):
|
||||||
|
result = (self.request.user.has_perm('users.can_see_name') and
|
||||||
|
self.request.user.has_perm('users.can_see_extra_data') and
|
||||||
|
self.request.user.has_perm('users.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
"""
|
||||||
|
Returns different serializer classes with respect to action and user's
|
||||||
|
permissions.
|
||||||
|
"""
|
||||||
|
if (self.action in ('create', 'partial_update', 'update') or
|
||||||
|
self.request.user.has_perm('users.can_see_extra_data')):
|
||||||
|
# Return the UserFullSerializer for edit requests or for
|
||||||
|
# list/retrieve requests of users with more permissions.
|
||||||
|
serializer_class = UserFullSerializer
|
||||||
|
else:
|
||||||
|
serializer_class = UserShortSerializer
|
||||||
|
return serializer_class
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def reset_password(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
View to reset the password (using the default password).
|
||||||
|
"""
|
||||||
|
user = self.get_object()
|
||||||
|
user.set_password(user.default_password)
|
||||||
|
user.save()
|
||||||
|
return Response({'detail': _('Password successfully reset.')})
|
||||||
|
|
||||||
|
|
||||||
|
class GroupViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for groups.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, partial_update,
|
||||||
|
update and destroy.
|
||||||
|
"""
|
||||||
|
queryset = Group.objects.all()
|
||||||
|
serializer_class = GroupSerializer
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
# Every authenticated user can list or retrieve groups.
|
||||||
|
# Anonymous users can do so if they are enabled.
|
||||||
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
|
# Users with all app permissions can edit groups.
|
||||||
|
result = (self.request.user.has_perm('users.can_see_name') and
|
||||||
|
self.request.user.has_perm('users.can_see_extra_data') and
|
||||||
|
self.request.user.has_perm('users.can_manage'))
|
||||||
|
else:
|
||||||
|
# Deny request in any other case.
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Protects builtin groups 'Anonymous' (pk=1) and 'Registered' (pk=2)
|
||||||
|
from being deleted.
|
||||||
|
"""
|
||||||
|
instance = self.get_object()
|
||||||
|
if instance.pk in (1, 2):
|
||||||
|
self.permission_denied(request)
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
# Special API views
|
||||||
|
|
||||||
|
class UserLoginView(APIView):
|
||||||
|
"""
|
||||||
|
Login the user via Ajax.
|
||||||
|
"""
|
||||||
|
http_method_names = ['post']
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
form = AuthenticationForm(self.request, data=self.request.data)
|
||||||
|
if form.is_valid():
|
||||||
|
self.user = form.get_user()
|
||||||
|
auth_login(self.request, self.user)
|
||||||
|
self.success = True
|
||||||
|
else:
|
||||||
|
self.success = False
|
||||||
|
return super().post(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
context['success'] = self.success
|
||||||
|
if self.success:
|
||||||
|
context['user_id'] = self.user.pk
|
||||||
|
return super().get_context_data(**context)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogoutView(APIView):
|
||||||
|
"""
|
||||||
|
Logout the user via Ajax.
|
||||||
|
"""
|
||||||
|
http_method_names = ['post']
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
auth_logout(self.request)
|
||||||
|
return super().post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WhoAmIView(APIView):
|
||||||
|
"""
|
||||||
|
Returns the id of the requesting user.
|
||||||
|
"""
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
"""
|
||||||
|
Appends the user id into the context.
|
||||||
|
|
||||||
|
Uses None for the anonymous user.
|
||||||
|
"""
|
||||||
|
return super().get_context_data(
|
||||||
|
user_id=self.request.user.pk,
|
||||||
|
**context)
|
||||||
|
|
||||||
|
|
||||||
# Views to generate PDFs
|
# Views to generate PDFs
|
||||||
|
|
||||||
class UsersListPDF(PDFView):
|
class UsersListPDF(PDFView):
|
||||||
@ -51,146 +194,3 @@ class UsersPasswordsPDF(PDFView):
|
|||||||
Append PDF objects.
|
Append PDF objects.
|
||||||
"""
|
"""
|
||||||
users_passwords_to_pdf(pdf)
|
users_passwords_to_pdf(pdf)
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the rest api
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and delete users.
|
|
||||||
"""
|
|
||||||
queryset = User.objects.all()
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to see users and in case of create, update or destroy
|
|
||||||
requests the permission to see extra user data and to manage users.
|
|
||||||
"""
|
|
||||||
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 action and user's
|
|
||||||
permissions.
|
|
||||||
"""
|
|
||||||
if (self.action in ('create', 'update') or
|
|
||||||
self.request.user.has_perm('users.can_see_extra_data')):
|
|
||||||
serializer_class = UserFullSerializer
|
|
||||||
else:
|
|
||||||
serializer_class = UserShortSerializer
|
|
||||||
return serializer_class
|
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
|
||||||
def reset_password(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
View to reset the password (using the default password).
|
|
||||||
"""
|
|
||||||
if not request.user.has_perm('users.can_manage'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
user = self.get_object()
|
|
||||||
user.set_password(user.default_password)
|
|
||||||
user.save()
|
|
||||||
return Response({'detail': _('Password successfully reset.')})
|
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and delete groups.
|
|
||||||
"""
|
|
||||||
queryset = Group.objects.all()
|
|
||||||
serializer_class = GroupSerializer
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to see users and in case of create, update or destroy
|
|
||||||
requests the permission to see extra user data and to manage users.
|
|
||||||
"""
|
|
||||||
# Any logged in user can retrive groups.
|
|
||||||
# Anonymous user can retrive groups when they are activated.
|
|
||||||
if (self.action in ('retrieve', 'list') and
|
|
||||||
(config['general_system_enable_anonymous'] or
|
|
||||||
self.request.user.is_authenticated())):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Users with the permissions 'can_manage' and 'can_see_extra_data' can
|
|
||||||
# edit groups.
|
|
||||||
if (self.action in ('create', 'update', 'destroy', 'partial_update') and
|
|
||||||
request.user.has_perm('users.can_see_name') and
|
|
||||||
request.user.has_perm('users.can_manage') and
|
|
||||||
request.user.has_perm('users.can_see_extra_data')):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Raise permission_denied in any other case.
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Protects builtin groups 'Anonymous' (pk=1) and 'Registered' (pk=2)
|
|
||||||
from being deleted.
|
|
||||||
"""
|
|
||||||
instance = self.get_object()
|
|
||||||
if instance.pk in (1, 2,):
|
|
||||||
self.permission_denied(request)
|
|
||||||
else:
|
|
||||||
self.perform_destroy(instance)
|
|
||||||
response = Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
# API Views
|
|
||||||
|
|
||||||
class UserLoginView(APIView):
|
|
||||||
"""
|
|
||||||
Login the user via ajax.
|
|
||||||
"""
|
|
||||||
http_method_names = ['post']
|
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
|
||||||
form = AuthenticationForm(self.request, data=self.request.data)
|
|
||||||
if form.is_valid():
|
|
||||||
self.user = form.get_user()
|
|
||||||
auth_login(self.request, self.user)
|
|
||||||
self.success = True
|
|
||||||
else:
|
|
||||||
self.success = False
|
|
||||||
return super().post(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
|
||||||
context['success'] = self.success
|
|
||||||
if self.success:
|
|
||||||
context['user_id'] = self.user.pk
|
|
||||||
return super().get_context_data(**context)
|
|
||||||
|
|
||||||
|
|
||||||
class UserLogoutView(APIView):
|
|
||||||
"""
|
|
||||||
Logout the user via ajax.
|
|
||||||
"""
|
|
||||||
http_method_names = ['post']
|
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
|
||||||
auth_logout(self.request)
|
|
||||||
return super().post(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class WhoAmIView(APIView):
|
|
||||||
"""
|
|
||||||
Returns the user id in the session.
|
|
||||||
"""
|
|
||||||
http_method_names = ['get']
|
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
|
||||||
"""
|
|
||||||
Appends the user id into the context.
|
|
||||||
|
|
||||||
Uses None for the anonymous user.
|
|
||||||
"""
|
|
||||||
return super().get_context_data(
|
|
||||||
user_id=self.request.user.pk,
|
|
||||||
**context)
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route # noqa
|
from rest_framework.decorators import detail_route, list_route # noqa
|
||||||
from rest_framework.decorators import list_route # noqa
|
|
||||||
from rest_framework.metadata import SimpleMetadata # noqa
|
from rest_framework.metadata import SimpleMetadata # noqa
|
||||||
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa
|
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa
|
||||||
from rest_framework.response import Response # noqa
|
from rest_framework.response import Response # noqa
|
||||||
@ -20,39 +19,43 @@ from rest_framework.serializers import ( # noqa
|
|||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
|
from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
|
||||||
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
||||||
from rest_framework.viewsets import ( # noqa
|
from rest_framework.viewsets import \
|
||||||
GenericViewSet,
|
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
|
||||||
ReadOnlyModelViewSet,
|
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
||||||
ViewSet,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .exceptions import OpenSlidesError
|
from .exceptions import OpenSlidesError
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
|
||||||
|
|
||||||
class ModelViewSet(_ModelViewSet):
|
class PermissionMixin:
|
||||||
"""
|
"""
|
||||||
Viewset for models. Before the method check_permission is called we
|
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
|
||||||
check projector requirements. If access for projector client users is
|
|
||||||
not currently required, check_permission is called, else not.
|
|
||||||
"""
|
|
||||||
def initial(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Runs anything that needs to occur prior to calling the method handler.
|
|
||||||
"""
|
|
||||||
self.format_kwarg = self.get_format_suffix(**kwargs)
|
|
||||||
|
|
||||||
# Ensure that the incoming request is permitted
|
The methods check_view_permissions or check_projector_requirements are
|
||||||
self.perform_authentication(request)
|
evaluated. If both return False self.permission_denied() is called.
|
||||||
if not self.check_projector_requirements():
|
Django REST framework's permission system is disabled.
|
||||||
self.check_permissions(request)
|
"""
|
||||||
self.check_throttles(request)
|
|
||||||
|
|
||||||
# Perform content negotiation and store the accepted info on the request
|
def get_permissions(self):
|
||||||
neg = self.perform_content_negotiation(request)
|
"""
|
||||||
request.accepted_renderer, request.accepted_media_type = neg
|
Overriden method to check view and projector permissions. Returns an
|
||||||
|
empty interable so Django REST framework won't do any other
|
||||||
|
permission checks by evaluating Django REST framework style permission
|
||||||
|
classes and the request passes.
|
||||||
|
"""
|
||||||
|
if not self.check_view_permissions() and not self.check_projector_requirements():
|
||||||
|
self.permission_denied(self.request)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Override this and return True if the requesting user should be able to
|
||||||
|
get access to your view.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
def check_projector_requirements(self):
|
def check_projector_requirements(self):
|
||||||
"""
|
"""
|
||||||
@ -70,6 +73,22 @@ class ModelViewSet(_ModelViewSet):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class GenericViewSet(PermissionMixin, _GenericViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModelViewSet(PermissionMixin, _ModelViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSet(PermissionMixin, _ViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_collection_and_id_from_url(url):
|
def get_collection_and_id_from_url(url):
|
||||||
"""
|
"""
|
||||||
Helper function. Returns a tuple containing the collection name and the id
|
Helper function. Returns a tuple containing the collection name and the id
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import ugettext_lazy
|
||||||
from django.views import generic as django_views
|
from django.views import generic as django_views
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
@ -16,41 +15,6 @@ from .pdf import firstPage, laterPages
|
|||||||
View = django_views.View
|
View = django_views.View
|
||||||
|
|
||||||
|
|
||||||
class PermissionMixin:
|
|
||||||
"""
|
|
||||||
Mixin for views, that only can be visited from users with special
|
|
||||||
permissions.
|
|
||||||
|
|
||||||
Set the attribute 'required_permission' to the required permission
|
|
||||||
string or override the method 'check_permission'.
|
|
||||||
"""
|
|
||||||
required_permission = None
|
|
||||||
|
|
||||||
def check_permission(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Checks if the user has the required permission.
|
|
||||||
"""
|
|
||||||
if self.required_permission is None:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return request.user.has_perm(self.required_permission)
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Check if the user has the permission.
|
|
||||||
|
|
||||||
If the user is not logged in, redirect the user to the login page.
|
|
||||||
"""
|
|
||||||
if not self.check_permission(request, *args, **kwargs):
|
|
||||||
if not request.user.is_authenticated():
|
|
||||||
path = request.get_full_path()
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
"%s?next=%s" % (settings.LOGIN_URL, path))
|
|
||||||
else:
|
|
||||||
raise PermissionDenied
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SingleObjectMixin(django_views.detail.SingleObjectMixin):
|
class SingleObjectMixin(django_views.detail.SingleObjectMixin):
|
||||||
"""
|
"""
|
||||||
Mixin for single objects from the database.
|
Mixin for single objects from the database.
|
||||||
@ -88,14 +52,33 @@ class CSRFMixin:
|
|||||||
return ensure_csrf_cookie(view)
|
return ensure_csrf_cookie(view)
|
||||||
|
|
||||||
|
|
||||||
class PDFView(PermissionMixin, View):
|
class PDFView(View):
|
||||||
"""
|
"""
|
||||||
View to generate an PDF.
|
View to generate an PDF.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filename = ugettext_lazy('undefined-filename')
|
filename = ugettext_lazy('undefined-filename')
|
||||||
top_space = 3
|
top_space = 3
|
||||||
document_title = None
|
document_title = None
|
||||||
|
required_permission = None
|
||||||
|
|
||||||
|
def check_permission(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Checks if the user has the required permission.
|
||||||
|
"""
|
||||||
|
if self.required_permission is None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return request.user.has_perm(self.required_permission)
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check if the user has the permission.
|
||||||
|
|
||||||
|
If the user is not logged in, redirect the user to the login page.
|
||||||
|
"""
|
||||||
|
if not self.check_permission(request, *args, **kwargs):
|
||||||
|
raise PermissionDenied
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_top_space(self):
|
def get_top_space(self):
|
||||||
return self.top_space
|
return self.top_space
|
||||||
|
@ -1,67 +1,9 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from openslides.utils import views
|
from openslides.utils import views
|
||||||
|
|
||||||
|
|
||||||
class PermissionMixinTest(TestCase):
|
|
||||||
def test_check_permission_non_required_permission(self):
|
|
||||||
view = views.PermissionMixin()
|
|
||||||
view.required_permission = None
|
|
||||||
request = MagicMock()
|
|
||||||
|
|
||||||
self.assertTrue(view.check_permission(request))
|
|
||||||
|
|
||||||
def test_check_permission_with_required_permission(self):
|
|
||||||
view = views.PermissionMixin()
|
|
||||||
view.required_permission = 'required_permission'
|
|
||||||
request = MagicMock()
|
|
||||||
|
|
||||||
view.check_permission(request)
|
|
||||||
|
|
||||||
request.user.has_perm.assert_called_once_with('required_permission')
|
|
||||||
|
|
||||||
@patch('builtins.super')
|
|
||||||
def test_dispatch_with_perm(self, mock_super):
|
|
||||||
view = views.PermissionMixin()
|
|
||||||
view.check_permission = MagicMock(return_value=True)
|
|
||||||
request = MagicMock()
|
|
||||||
|
|
||||||
view.dispatch(request)
|
|
||||||
|
|
||||||
mock_super().dispatch.called_once_with(request)
|
|
||||||
|
|
||||||
@patch('openslides.utils.views.settings')
|
|
||||||
@patch('openslides.utils.views.HttpResponseRedirect')
|
|
||||||
@patch('builtins.super')
|
|
||||||
def test_dispatch_without_perm_logged_out(self, mock_super, mock_response, mock_settings):
|
|
||||||
view = views.PermissionMixin()
|
|
||||||
view.check_permission = MagicMock(return_value=False)
|
|
||||||
request = MagicMock()
|
|
||||||
request.user.is_authenticated.return_value = False
|
|
||||||
request.get_full_path.return_value = '/requested/path/'
|
|
||||||
mock_settings.LOGIN_URL = 'my_login_url'
|
|
||||||
|
|
||||||
value = view.dispatch(request)
|
|
||||||
|
|
||||||
mock_response.assert_called_once_with('my_login_url?next=/requested/path/')
|
|
||||||
self.assertEqual(value, mock_response())
|
|
||||||
|
|
||||||
@patch('openslides.utils.views.settings')
|
|
||||||
@patch('openslides.utils.views.HttpResponseRedirect')
|
|
||||||
@patch('builtins.super')
|
|
||||||
def test_dispatch_without_perm_logged_in(self, mock_super, mock_response, mock_settings):
|
|
||||||
view = views.PermissionMixin()
|
|
||||||
view.check_permission = MagicMock(return_value=False)
|
|
||||||
request = MagicMock()
|
|
||||||
request.user.is_authenticated.return_value = True
|
|
||||||
|
|
||||||
with self.assertRaises(PermissionDenied):
|
|
||||||
view.dispatch(request)
|
|
||||||
|
|
||||||
|
|
||||||
@patch('builtins.super')
|
@patch('builtins.super')
|
||||||
class SingleObjectMixinTest(TestCase):
|
class SingleObjectMixinTest(TestCase):
|
||||||
def test_get_object_cache(self, mock_super):
|
def test_get_object_cache(self, mock_super):
|
||||||
|
Loading…
Reference in New Issue
Block a user