OpenSlides/openslides/core/views.py
Norman Jäckel dc7d27a985 Added REST API for projector. Introduced new projector API.
Added custom slide projector element class.
Added welcome slide as custom slide.
Added user slide projector element class.
Added clock, countdown ans message projector elements.
Renamed SignalConnectMetaClass classmethod get_all_objects to get_all (private API).
Added migrations to core app.
Fixed and wrote tests.
Updated CHANGELOG.
2015-05-29 12:44:25 +02:00

483 lines
18 KiB
Python

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/<pk>/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/<pk>/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/<pk>/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/<pk>/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)