import re from collections import OrderedDict from operator import attrgetter from django.conf import settings from django.contrib.staticfiles import finders from django.core.urlresolvers import get_resolver from django.http import Http404, HttpResponse from openslides import __version__ as version from openslides.utils import views as utils_views from openslides.utils.plugins import ( get_plugin_description, get_plugin_verbose_name, get_plugin_version, ) from openslides.utils.rest_api import ( ModelViewSet, ReadOnlyModelViewSet, Response, SimpleMetadata, ValidationError, ViewSet, detail_route, ) from .config import config from .exceptions import ConfigError, ConfigNotFound from .models import CustomSlide, Projector, Tag from .serializers import ( CustomSlideSerializer, ProjectorSerializer, TagSerializer, ) class IndexView(utils_views.CSRFMixin, utils_views.View): """ The primary view for OpenSlides using AngularJS. The default base template is 'openslides/core/static/templates/index.html'. You can override it by simply adding a custom 'templates/index.html' file to the custom staticfiles directory. See STATICFILES_DIRS in settings.py. """ def get(self, *args, **kwargs): with open(finders.find('templates/index.html')) as f: content = f.read() return HttpResponse(content) class ProjectorView(utils_views.View): """ Access the projector. """ def get(self, *args, **kwargs): with open(finders.find('templates/projector.html')) as f: content = f.read() return HttpResponse(content) class ProjectorViewSet(ReadOnlyModelViewSet): """ API endpoint to list, retrieve and update the projector slide info. """ queryset = Projector.objects.all() serializer_class = ProjectorSerializer def check_permissions(self, request): """ Calls self.permission_denied() if the requesting user has not the permission to see the projector and in case of an update request the permission to manage the projector. """ manage_methods = ( 'activate_elements', 'prune_elements', 'deactivate_elements', 'clear_elements') if (not request.user.has_perm('core.can_see_projector') or (self.action in manage_methods and not request.user.has_perm('core.can_manage_projector'))): self.permission_denied(request) @detail_route(methods=['post']) def activate_elements(self, request, pk): """ REST API operation to activate projector elements. It expects a POST request to /rest/core/projector//activate_elements/ with a list of dictionaries to append to the projector config entry. """ # Get config entry from projector model, add new elements and try to # serialize. This raises ValidationErrors if the data is invalid. projector_instance = self.get_object() projector_config = projector_instance.config for projector_element in request.data: projector_config.append(projector_element) serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) @detail_route(methods=['post']) def prune_elements(self, request, pk): """ REST API operation to activate projector elements. It expects a POST request to /rest/core/projector//prune_elements/ with a list of dictionaries to write them to the projector config entry. All old entries are deleted but not entries with stable == True. """ # Get config entry from projector model, delete old and add new # elements and try to serialize. This raises ValidationErrors if the # data is invalid. Do not filter 'stable' elements. projector_instance = self.get_object() projector_config = [element for element in projector_instance.config if element.get('stable')] projector_config.extend(request.data) serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) @detail_route(methods=['post']) def deactivate_elements(self, request, pk): """ REST API operation to deactivate projector elements. It expects a POST request to /rest/core/projector//deactivate_elements/ with a list of dictionaries. These are exactly the projector_elements in the config that should be deleted. """ # Check the data. It must be a list of dictionaries. Get config # entry from projector model. Pop out the entries that should be # deleted and try to serialize. This raises ValidationErrors if the # data is invalid. if not isinstance(request.data, list) or list(filter(lambda item: not isinstance(item, dict), request.data)): raise ValidationError({'config': ['Data must be a list of dictionaries.']}) projector_instance = self.get_object() projector_config = projector_instance.config for entry_to_be_deleted in request.data: try: projector_config.remove(entry_to_be_deleted) except ValueError: # The entry that should be deleted is not on the projector. pass serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) @detail_route(methods=['post']) def clear_elements(self, request, pk): """ REST API operation to deactivate all projector elements but not entries with stable == True. It expects a POST request to /rest/core/projector//clear_elements/. """ # Get config entry from projector model. Then clear the config field # and try to serialize. Do not remove 'stable' elements. projector_instance = self.get_object() projector_config = [element for element in projector_instance.config if element.get('stable')] serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class CustomSlideViewSet(ModelViewSet): """ API endpoint to list, retrieve, create, update and destroy custom slides. """ 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 projector. """ if not request.user.has_perm('core.can_manage_projector'): self.permission_denied(request) class TagViewSet(ModelViewSet): """ API endpoint to list, retrieve, create, update and destroy tags. """ 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 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 not request.user.has_perm('core.can_manage_tags')): self.permission_denied(request) class UrlPatternsView(utils_views.APIView): """ Returns a dictionary with all url patterns as json. The patterns kwargs are transformed using a colon. """ URL_KWARGS_REGEX = re.compile(r'%\((\w*)\)s') http_method_names = ['get'] def get_context_data(self, **context): result = {} url_dict = get_resolver(None).reverse_dict for pattern_name in filter(lambda key: isinstance(key, str), url_dict.keys()): normalized_regex_bits, p_pattern, pattern_default_args = url_dict[pattern_name] url, url_kwargs = normalized_regex_bits[0] result[pattern_name] = self.URL_KWARGS_REGEX.sub(r':\1', url) return result class VersionView(utils_views.APIView): """ Returns a dictionary with the OpenSlides version and the version of all plugins. """ http_method_names = ['get'] def get_context_data(self, **context): result = dict(openslides_version=version, plugins=[]) # Versions of plugins. for plugin in settings.INSTALLED_PLUGINS: result['plugins'].append({ 'verbose_name': get_plugin_verbose_name(plugin), 'description': get_plugin_description(plugin), 'version': get_plugin_version(plugin)}) 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})