import re from django.conf import settings from django.contrib import messages from django.contrib.staticfiles import finders from django.core.exceptions import PermissionDenied from django.core.urlresolvers import get_resolver, reverse from django.db import IntegrityError from django.http import HttpResponse from django.shortcuts import redirect, render_to_response from django.template import RequestContext from django.utils.importlib import import_module from django.utils.translation import ugettext as _ from haystack.views import SearchView as _SearchView from openslides import __version__ as openslides_version from openslides.config.api import config 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, ValidationError, detail_route ) from openslides.utils.signals import template_manipulation from openslides.utils.widgets import Widget from .exceptions import TagException from .forms import SelectWidgetsForm 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 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 ErrorView(utils_views.View): """ View for Http 403, 404 and 500 error pages. """ status_code = None def dispatch(self, request, *args, **kwargs): http_error_strings = { 403: {'name': _('Forbidden'), 'description': _('Sorry, you have no permission to see this page.'), 'status_code': '403'}, 404: {'name': _('Not Found'), 'description': _('Sorry, the requested page could not be found.'), 'status_code': '404'}, 500: {'name': _('Internal Server Error'), 'description': _('Sorry, there was an unknown error. Please contact the event manager.'), 'status_code': '500'}} context = {} context['http_error'] = http_error_strings[self.status_code] template_manipulation.send(sender=self.__class__, request=request, context=context) response = render_to_response( 'core/error.html', context_instance=RequestContext(request, context)) response.status_code = self.status_code return response # TODO: Remove the following classes one by one. class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView): """ Overview over all possible slides, the overlays and a live view: the Dashboard of OpenSlides. This main view uses the widget api to collect all widgets from all apps. See openslides.utils.widgets.Widget for more details. """ required_permission = 'core.can_see_dashboard' template_name = 'core/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) widgets = [] for widget in Widget.get_all(self.request): if widget.is_active(): widgets.append(widget) context['extra_stylefiles'].extend(widget.get_stylesheets()) context['extra_javascript'].extend(widget.get_javascript_files()) context['widgets'] = widgets return context class SelectWidgetsView(utils_views.TemplateView): """ Shows a form to select which widgets should be displayed on the own dashboard. The setting is saved in the session. """ # TODO: Use another base view class here, e. g. a FormView required_permission = 'core.can_see_dashboard' template_name = 'core/select_widgets.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) widgets = Widget.get_all(self.request) for widget in widgets: initial = {'widget': widget.is_active()} prefix = widget.name if self.request.method == 'POST': widget.form = SelectWidgetsForm( self.request.POST, prefix=prefix, initial=initial) else: widget.form = SelectWidgetsForm(prefix=prefix, initial=initial) context['widgets'] = widgets return context def post(self, request, *args, **kwargs): """ Activates or deactivates the widgets in a post request. """ context = self.get_context_data(**kwargs) session_widgets = self.request.session.get('widgets', {}) for widget in context['widgets']: if widget.form.is_valid(): session_widgets[widget.name] = widget.form.cleaned_data['widget'] else: messages.error(request, _('There are errors in the form.')) break else: self.request.session['widgets'] = session_widgets return redirect(reverse('core_dashboard')) class VersionView(utils_views.TemplateView): """ Shows version infos. """ template_name = 'core/version.html' def get_context_data(self, **kwargs): """ Adds version strings to the context. """ context = super().get_context_data(**kwargs) context['modules'] = [{'verbose_name': 'OpenSlides', 'description': '', 'version': openslides_version}] # Versions of plugins. for plugin in settings.INSTALLED_PLUGINS: context['modules'].append({'verbose_name': get_plugin_verbose_name(plugin), 'description': get_plugin_description(plugin), 'version': get_plugin_version(plugin)}) return context class SearchView(_SearchView): """ Shows search result page. """ template = 'core/search.html' def __call__(self, request): if not request.user.is_authenticated() and not config['system_enable_anonymous']: raise PermissionDenied return super().__call__(request) def extra_context(self): """ Adds extra context variables to set navigation and search filter. Returns a context dictionary. """ context = {} template_manipulation.send( sender=self.__class__, request=self.request, context=context) context['models'] = self.get_indexed_searchmodels() context['get_values'] = self.request.GET.getlist('models') return context def get_indexed_searchmodels(self): """ Iterate over all INSTALLED_APPS and return a list of models which are indexed by haystack/whoosh for using in customized model search filter in search template search.html. Each list entry contains a verbose name of the model and a special form field value for haystack (app_name.model_name), e.g. ['Agenda', 'agenda.item']. """ models = [] # TODO: cache this query! for app in settings.INSTALLED_APPS: try: module = import_module(app + '.search_indexes') except ImportError: pass else: models.append([module.Index.modelfilter_name, module.Index.modelfilter_value]) return models class CustomSlideViewMixin(object): """ Mixin for for CustomSlide Views. """ fields = ('title', 'text', 'weight',) required_permission = 'core.can_manage_projector' template_name = 'core/customslide_update.html' model = CustomSlide success_url_name = 'core_dashboard' url_name_args = [] class CustomSlideCreateView(CustomSlideViewMixin, utils_views.CreateView): """ Create a custom slide. """ pass class CustomSlideUpdateView(CustomSlideViewMixin, utils_views.UpdateView): """ Update a custom slide. """ pass class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView): """ Delete a custom slide. """ pass class TagListView(utils_views.AjaxMixin, utils_views.ListView): """ View to list and manipulate tags. Shows all tags when requested via a GET-request. Manipulates tags with POST-requests. """ model = Tag required_permission = 'core.can_manage_tags' def post(self, *args, **kwargs): return self.ajax_get(*args, **kwargs) def ajax_get(self, request, *args, **kwargs): name, value = request.POST['name'], request.POST.get('value', None) # Create a new tag if name == 'new': try: tag = Tag.objects.create(name=value) except IntegrityError: # The name of the tag is already taken. It must be unique. self.error = 'Tag name is already taken' else: self.pk = tag.pk self.action = 'created' # Update an existing tag elif name.startswith('edit-tag-'): try: self.get_tag_queryset(name, 9).update(name=value) except TagException as error: self.error = str(error) except IntegrityError: self.error = 'Tag name is already taken' except Tag.DoesNotExist: self.error = 'Tag does not exist' else: self.action = 'updated' # Delete a tag elif name.startswith('delete-tag-'): try: self.get_tag_queryset(name, 11).delete() except TagException as error: self.error = str(error) except Tag.DoesNotExist: self.error = 'Tag does not exist' else: self.action = 'deleted' return super().ajax_get(request, *args, **kwargs) def get_tag_queryset(self, name, place_in_str): """ Get a django-tag-queryset from a string. 'name' is the string in which the pk is (at the end). 'place_in_str' is the place where to look for the pk. It has to be an int. Returns a Tag QuerySet or raises TagException. Also sets self.pk to the pk inside the name. """ try: self.pk = int(name[place_in_str:]) except ValueError: raise TagException('Invalid name in request') return Tag.objects.filter(pk=self.pk) def get_ajax_context(self, **context): return super().get_ajax_context( pk=getattr(self, 'pk', None), action=getattr(self, 'action', None), error=getattr(self, 'error', None), **context)