dc7d27a985
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.
483 lines
18 KiB
Python
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)
|