2018-11-04 14:02:30 +01:00
|
|
|
import datetime
|
2018-10-13 08:41:51 +02:00
|
|
|
import os
|
2019-07-23 14:58:01 +02:00
|
|
|
from collections import defaultdict
|
2019-01-19 17:42:18 +01:00
|
|
|
from typing import Any, Dict
|
2015-02-14 10:10:08 +01:00
|
|
|
|
2019-08-01 11:47:09 +02:00
|
|
|
from asgiref.sync import async_to_sync
|
2015-06-18 21:48:20 +02:00
|
|
|
from django.conf import settings
|
2018-10-13 08:41:51 +02:00
|
|
|
from django.contrib.staticfiles import finders
|
|
|
|
from django.contrib.staticfiles.views import serve
|
2015-09-14 23:16:31 +02:00
|
|
|
from django.db.models import F
|
2015-06-29 12:08:15 +02:00
|
|
|
from django.http import Http404, HttpResponse
|
2015-09-24 21:28:30 +02:00
|
|
|
from django.utils.timezone import now
|
2018-10-13 08:41:51 +02:00
|
|
|
from django.views import static
|
|
|
|
from django.views.generic.base import View
|
2013-03-01 17:13:12 +01:00
|
|
|
|
2019-07-23 14:58:01 +02:00
|
|
|
from openslides.utils.utils import split_element_id
|
|
|
|
|
2018-07-09 23:22:26 +02:00
|
|
|
from .. import __license__ as license, __url__ as url, __version__ as version
|
2019-01-19 17:42:18 +01:00
|
|
|
from ..users.models import User
|
2016-10-17 16:56:19 +02:00
|
|
|
from ..utils import views as utils_views
|
2018-10-13 08:41:51 +02:00
|
|
|
from ..utils.arguments import arguments
|
2019-01-06 16:22:33 +01:00
|
|
|
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
|
2019-05-30 12:50:28 +02:00
|
|
|
from ..utils.autoupdate import inform_changed_data
|
2019-08-01 11:47:09 +02:00
|
|
|
from ..utils.cache import element_cache
|
2016-10-17 16:56:19 +02:00
|
|
|
from ..utils.plugins import (
|
2015-06-18 21:48:20 +02:00
|
|
|
get_plugin_description,
|
2017-12-01 10:10:35 +01:00
|
|
|
get_plugin_license,
|
|
|
|
get_plugin_url,
|
2015-06-18 21:48:20 +02:00
|
|
|
get_plugin_verbose_name,
|
|
|
|
get_plugin_version,
|
|
|
|
)
|
2016-10-17 16:56:19 +02:00
|
|
|
from ..utils.rest_api import (
|
2018-11-04 14:02:30 +01:00
|
|
|
GenericViewSet,
|
|
|
|
ListModelMixin,
|
2015-02-18 01:45:39 +01:00
|
|
|
ModelViewSet,
|
|
|
|
Response,
|
2018-11-04 14:02:30 +01:00
|
|
|
RetrieveModelMixin,
|
2015-02-18 01:45:39 +01:00
|
|
|
ValidationError,
|
2015-06-16 10:37:23 +02:00
|
|
|
detail_route,
|
2019-05-27 18:38:43 +02:00
|
|
|
list_route,
|
2015-02-18 01:45:39 +01:00
|
|
|
)
|
2019-03-06 14:53:24 +01:00
|
|
|
from .access_permissions import (
|
|
|
|
ConfigAccessPermissions,
|
|
|
|
CountdownAccessPermissions,
|
2019-03-26 14:57:04 +01:00
|
|
|
ProjectionDefaultAccessPermissions,
|
2019-03-06 14:53:24 +01:00
|
|
|
ProjectorAccessPermissions,
|
|
|
|
ProjectorMessageAccessPermissions,
|
|
|
|
TagAccessPermissions,
|
|
|
|
)
|
|
|
|
from .config import config
|
|
|
|
from .exceptions import ConfigError, ConfigNotFound
|
|
|
|
from .models import (
|
|
|
|
ConfigStore,
|
|
|
|
Countdown,
|
|
|
|
History,
|
|
|
|
HistoryData,
|
|
|
|
ProjectionDefault,
|
|
|
|
Projector,
|
|
|
|
ProjectorMessage,
|
|
|
|
Tag,
|
|
|
|
)
|
2019-04-09 16:13:40 +02:00
|
|
|
from .serializers import elements_array_validator, elements_validator
|
2013-12-09 23:56:01 +01:00
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Special Django views
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2018-10-13 08:41:51 +02:00
|
|
|
class IndexView(View):
|
2015-01-30 11:58:36 +01:00
|
|
|
"""
|
2018-10-13 08:41:51 +02:00
|
|
|
The primary view for the OpenSlides client. Serves static files. If a file
|
|
|
|
does not exist or a directory is requested, the index.html is delivered instead.
|
2016-08-25 16:40:34 +02:00
|
|
|
"""
|
|
|
|
|
2018-10-13 08:41:51 +02:00
|
|
|
cache: Dict[str, str] = {}
|
2015-06-17 09:45:00 +02:00
|
|
|
"""
|
2018-10-13 08:41:51 +02:00
|
|
|
Saves the path to the index.html.
|
2015-06-17 09:45:00 +02:00
|
|
|
|
2018-10-13 08:41:51 +02:00
|
|
|
May be extended later to cache every template.
|
2015-07-01 23:18:48 +02:00
|
|
|
"""
|
2018-01-16 16:02:23 +01:00
|
|
|
|
2017-09-26 14:19:48 +02:00
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
|
|
super().__init__(*args, **kwargs)
|
2018-01-16 16:02:23 +01:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
no_caching = arguments.get("no_template_caching", False)
|
|
|
|
if "index" not in self.cache or no_caching:
|
|
|
|
self.cache["index"] = finders.find("index.html")
|
2018-10-13 08:41:51 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
self.index_document_root, self.index_path = os.path.split(self.cache["index"])
|
2018-10-13 08:41:51 +02:00
|
|
|
|
|
|
|
def get(self, request, path, **kwargs) -> HttpResponse:
|
|
|
|
"""
|
|
|
|
Tries to serve the requested file. If it is not found or a directory is
|
|
|
|
requested, the index.html is delivered.
|
|
|
|
"""
|
|
|
|
try:
|
2019-01-20 09:29:19 +01:00
|
|
|
response = serve(request, path, insecure=True, **kwargs)
|
2018-10-13 08:41:51 +02:00
|
|
|
except Http404:
|
2019-01-06 16:22:33 +01:00
|
|
|
response = static.serve(
|
|
|
|
request,
|
|
|
|
self.index_path,
|
|
|
|
document_root=self.index_document_root,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2018-10-13 08:41:51 +02:00
|
|
|
return response
|
2015-07-01 23:18:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
# Viewsets for the REST API
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-09-12 11:05:34 +02:00
|
|
|
class ProjectorViewSet(ModelViewSet):
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for the projector slide info.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: See strings in check_view_permissions().
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = ProjectorAccessPermissions()
|
2015-02-18 01:45:39 +01:00
|
|
|
queryset = Projector.objects.all()
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "core.can_see_projector")
|
2016-09-29 15:32:58 +02:00
|
|
|
elif self.action in (
|
2019-01-06 16:22:33 +01:00
|
|
|
"create",
|
|
|
|
"update",
|
|
|
|
"partial_update",
|
|
|
|
"destroy",
|
|
|
|
"control_view",
|
|
|
|
"set_scroll",
|
2019-11-01 09:48:58 +01:00
|
|
|
"set_default_projector",
|
2019-01-19 10:37:05 +01:00
|
|
|
"project",
|
2016-09-29 15:32:58 +02:00
|
|
|
):
|
2019-01-06 16:22:33 +01:00
|
|
|
result = has_perm(self.request.user, "core.can_see_projector") and has_perm(
|
|
|
|
self.request.user, "core.can_manage_projector"
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2015-02-18 01:45:39 +01:00
|
|
|
|
2019-04-09 16:13:40 +02:00
|
|
|
def perform_create(self, serializer):
|
|
|
|
projector = serializer.save()
|
|
|
|
projector.elements = [{"name": "core/clock", "stable": True}]
|
|
|
|
projector.save()
|
|
|
|
|
2019-09-23 10:10:08 +02:00
|
|
|
def update(self, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Updates the projector.
|
|
|
|
Informs about changed projectors due to changes in projection defaults:
|
|
|
|
Collects all former projectors that were assigned to new projection defaults
|
|
|
|
during the update.
|
|
|
|
"""
|
|
|
|
projector = self.get_object()
|
|
|
|
|
|
|
|
# old_pd_ids and new_pd_ids (see below) are for detecting changed projectiondefaults.
|
|
|
|
old_pd_ids = set([default.id for default in projector.projectiondefaults.all()])
|
|
|
|
|
|
|
|
# maps all projection defaults to their projectors. So we can find out later the
|
|
|
|
# old projectors to changes projection defaults.
|
|
|
|
old_pd_projector_mapping = {
|
|
|
|
pd.id: pd.projector_id for pd in ProjectionDefault.objects.all()
|
|
|
|
}
|
|
|
|
|
|
|
|
response = super().update(*args, **kwargs)
|
|
|
|
|
|
|
|
# calculate changed projection defaults
|
|
|
|
new_pd_ids = set([default.id for default in projector.projectiondefaults.all()])
|
|
|
|
changed_pd_ids = list(new_pd_ids - old_pd_ids)
|
|
|
|
inform_changed_data(ProjectionDefault.objects.filter(pk__in=changed_pd_ids))
|
|
|
|
|
|
|
|
# Find *old* projector ids for the changed defaults
|
|
|
|
affected_projector_ids = [
|
|
|
|
old_pd_projector_mapping[pd_id] for pd_id in changed_pd_ids
|
|
|
|
]
|
|
|
|
inform_changed_data(Projector.objects.filter(pk__in=affected_projector_ids))
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
2016-09-12 11:05:34 +02:00
|
|
|
def destroy(self, *args, **kwargs):
|
2016-11-19 13:04:24 +01:00
|
|
|
"""
|
|
|
|
REST API operation for DELETE requests.
|
|
|
|
|
|
|
|
Assigns all ProjectionDefault objects from this projector to the
|
2019-03-26 14:57:04 +01:00
|
|
|
first projector found.
|
2016-11-19 13:04:24 +01:00
|
|
|
"""
|
2019-03-26 14:57:04 +01:00
|
|
|
if len(Projector.objects.all()) <= 1:
|
|
|
|
raise ValidationError({"detail": "You can't delete the last projector."})
|
2016-09-12 11:05:34 +02:00
|
|
|
projector_instance = self.get_object()
|
2019-03-26 14:57:04 +01:00
|
|
|
new_projector_id = (
|
|
|
|
Projector.objects.exclude(pk=projector_instance.pk).first().pk
|
|
|
|
)
|
|
|
|
|
2016-09-29 15:32:58 +02:00
|
|
|
for projection_default in ProjectionDefault.objects.all():
|
|
|
|
if projection_default.projector.id == projector_instance.id:
|
2019-03-26 14:57:04 +01:00
|
|
|
projection_default.projector_id = new_projector_id
|
2016-09-29 15:32:58 +02:00
|
|
|
projection_default.save()
|
2016-09-12 11:05:34 +02:00
|
|
|
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
|
|
|
|
|
2019-01-19 10:37:05 +01:00
|
|
|
@detail_route(methods=["post"])
|
|
|
|
def project(self, request, pk):
|
|
|
|
"""
|
|
|
|
Sets the `elements` and `elements_preview` and adds one item to the
|
|
|
|
`elements_history`.
|
|
|
|
|
|
|
|
`request.data` can have three arguments: `append_to_history`, `elements`
|
|
|
|
and `preview`. Non of them is required.
|
|
|
|
|
|
|
|
`append_to_history` adds one element to the end of the history_elements.
|
|
|
|
`elements` and `preview` preplaces the coresponding fields in the
|
|
|
|
database.
|
2019-01-24 16:25:50 +01:00
|
|
|
|
|
|
|
If `delete_last_history_element` is True, the last element is deleted.
|
|
|
|
Note: You cannot give `append_to_history` and `delete_last_history_element`
|
|
|
|
at the same time.
|
2019-08-01 08:03:34 +02:00
|
|
|
|
|
|
|
If `reset_scroll` is True, the scoll of the projector will reset.
|
2019-01-19 10:37:05 +01:00
|
|
|
"""
|
|
|
|
projector = self.get_object()
|
|
|
|
elements = request.data.get("elements")
|
|
|
|
preview = request.data.get("preview")
|
|
|
|
history_element = request.data.get("append_to_history")
|
2019-01-26 20:37:49 +01:00
|
|
|
delete_last_history_element = request.data.get(
|
|
|
|
"delete_last_history_element", False
|
|
|
|
)
|
2019-08-01 08:03:34 +02:00
|
|
|
reset_scroll = request.data.get("reset_scroll", False)
|
2019-01-19 10:37:05 +01:00
|
|
|
|
|
|
|
if elements is not None:
|
2019-04-09 16:13:40 +02:00
|
|
|
elements_validator(elements)
|
|
|
|
projector.elements = elements
|
|
|
|
|
2019-01-19 10:37:05 +01:00
|
|
|
if preview is not None:
|
2019-04-09 16:13:40 +02:00
|
|
|
elements_validator(preview)
|
|
|
|
projector.elements_preview = preview
|
|
|
|
|
|
|
|
elements_history = None
|
2019-01-24 16:25:50 +01:00
|
|
|
if history_element is not None and delete_last_history_element is False:
|
2019-04-09 16:13:40 +02:00
|
|
|
elements_history = projector.elements_history + [history_element]
|
2019-01-24 16:25:50 +01:00
|
|
|
if history_element is None and delete_last_history_element is True:
|
2019-04-09 16:13:40 +02:00
|
|
|
elements_history = projector.elements_history[:-1]
|
|
|
|
if elements_history is not None:
|
|
|
|
elements_array_validator(elements_history)
|
|
|
|
projector.elements_history = elements_history
|
2019-01-19 10:37:05 +01:00
|
|
|
|
2019-08-01 08:03:34 +02:00
|
|
|
if reset_scroll:
|
|
|
|
projector.scroll = 0
|
|
|
|
|
2019-04-09 16:13:40 +02:00
|
|
|
projector.save()
|
2019-01-19 10:37:05 +01:00
|
|
|
return Response()
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2015-09-14 23:16:31 +02:00
|
|
|
def control_view(self, request, pk):
|
|
|
|
"""
|
|
|
|
REST API operation to control the projector view, i. e. scale and
|
|
|
|
scroll the projector.
|
|
|
|
|
|
|
|
It expects a POST request to
|
|
|
|
/rest/core/projector/<pk>/control_view/ with a dictionary with an
|
|
|
|
action ('scale' or 'scroll') and a direction ('up', 'down' or
|
2019-03-08 10:38:22 +01:00
|
|
|
'reset'). An optional 'step' can be given to control the amount
|
|
|
|
of scrolling and scaling. The default is 1.
|
2015-09-14 23:16:31 +02:00
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
{
|
|
|
|
"action": "scale",
|
|
|
|
"direction": "up"
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
if not isinstance(request.data, dict):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
|
|
|
if request.data.get("action") not in ("scale", "scroll") or request.data.get(
|
|
|
|
"direction"
|
|
|
|
) not in ("up", "down", "reset"):
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "Data must be a dictionary with an action ('scale' or 'scroll') "
|
|
|
|
"and a direction ('up', 'down' or 'reset')."
|
|
|
|
}
|
|
|
|
)
|
2015-09-14 23:16:31 +02:00
|
|
|
|
|
|
|
projector_instance = self.get_object()
|
2019-03-08 10:38:22 +01:00
|
|
|
step = request.data.get("step", 1)
|
|
|
|
if step < 1:
|
|
|
|
step = 1
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.data["action"] == "scale":
|
|
|
|
if request.data["direction"] == "up":
|
2019-03-08 10:38:22 +01:00
|
|
|
projector_instance.scale = F("scale") + step
|
2019-01-06 16:22:33 +01:00
|
|
|
elif request.data["direction"] == "down":
|
2019-03-08 10:38:22 +01:00
|
|
|
projector_instance.scale = F("scale") - step
|
2015-09-14 23:16:31 +02:00
|
|
|
else:
|
|
|
|
# request.data['direction'] == 'reset'
|
|
|
|
projector_instance.scale = 0
|
|
|
|
else:
|
|
|
|
# request.data['action'] == 'scroll'
|
2019-01-06 16:22:33 +01:00
|
|
|
if request.data["direction"] == "up":
|
2019-03-08 10:38:22 +01:00
|
|
|
projector_instance.scroll = F("scroll") + step
|
2019-01-06 16:22:33 +01:00
|
|
|
elif request.data["direction"] == "down":
|
2019-03-08 10:38:22 +01:00
|
|
|
projector_instance.scroll = F("scroll") - step
|
2015-09-14 23:16:31 +02:00
|
|
|
else:
|
|
|
|
# request.data['direction'] == 'reset'
|
|
|
|
projector_instance.scroll = 0
|
|
|
|
|
2016-11-08 21:29:26 +01:00
|
|
|
projector_instance.save(skip_autoupdate=True)
|
|
|
|
projector_instance.refresh_from_db()
|
|
|
|
inform_changed_data(projector_instance)
|
2019-01-18 19:11:22 +01:00
|
|
|
return Response()
|
2015-09-14 23:16:31 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2016-09-05 16:23:44 +02:00
|
|
|
def set_scroll(self, request, pk):
|
|
|
|
"""
|
|
|
|
REST API operation to scroll the projector.
|
|
|
|
|
|
|
|
It expects a POST request to
|
|
|
|
/rest/core/projector/<pk>/set_scroll/ with a new value for scroll.
|
|
|
|
"""
|
|
|
|
if not isinstance(request.data, int):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": "Data must be an int."})
|
2016-09-05 16:23:44 +02:00
|
|
|
|
|
|
|
projector_instance = self.get_object()
|
|
|
|
projector_instance.scroll = request.data
|
|
|
|
|
|
|
|
projector_instance.save()
|
2019-09-02 11:09:03 +02:00
|
|
|
return Response(
|
|
|
|
{"detail": "Setting scroll to {0} was successful.", "args": [request.data]}
|
|
|
|
)
|
2016-09-05 16:23:44 +02:00
|
|
|
|
2019-11-01 09:48:58 +01:00
|
|
|
@detail_route(methods=["post"])
|
|
|
|
def set_default_projector(self, request, pk):
|
|
|
|
"""
|
|
|
|
REST API operation to set the projector with the given pk as the new default.
|
|
|
|
"""
|
|
|
|
reference_projector = self.get_object()
|
|
|
|
for projector in self.queryset.all():
|
|
|
|
projector.reference_projector = reference_projector
|
|
|
|
projector.save()
|
|
|
|
|
|
|
|
return Response()
|
|
|
|
|
2016-09-12 11:05:34 +02:00
|
|
|
|
2019-04-29 08:25:41 +02:00
|
|
|
class ProjectionDefaultViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
2019-03-26 14:57:04 +01:00
|
|
|
"""
|
|
|
|
API endpoint for projection defaults.
|
2016-09-12 11:05:34 +02:00
|
|
|
|
2019-04-29 08:25:41 +02:00
|
|
|
There are the following views: list, and retrieve. Assigning projection defaults
|
|
|
|
to projectors can be done by updating the projector.
|
2019-03-26 14:57:04 +01:00
|
|
|
"""
|
2016-09-12 11:05:34 +02:00
|
|
|
|
2019-03-26 14:57:04 +01:00
|
|
|
access_permissions = ProjectionDefaultAccessPermissions()
|
|
|
|
queryset = ProjectionDefault.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
|
|
|
if self.action in ("list", "retrieve"):
|
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2016-09-12 11:05:34 +02:00
|
|
|
|
2015-02-18 01:45:39 +01:00
|
|
|
|
|
|
|
class TagViewSet(ModelViewSet):
|
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for tags.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = TagAccessPermissions()
|
2015-02-18 01:45:39 +01:00
|
|
|
queryset = Tag.objects.all()
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-02-18 01:45:39 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
2016-12-17 09:30:20 +01:00
|
|
|
# Every authenticated user can see the metadata.
|
2016-02-11 22:58:32 +01:00
|
|
|
# Anonymous users can do so if they are enabled.
|
2018-07-09 23:22:26 +02:00
|
|
|
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "core.can_manage_tags")
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
2015-06-18 21:48:20 +02:00
|
|
|
return result
|
2015-06-29 12:08:15 +02:00
|
|
|
|
|
|
|
|
2017-08-22 14:17:20 +02:00
|
|
|
class ConfigViewSet(ModelViewSet):
|
2015-06-29 12:08:15 +02:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for the config.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: metadata, list, retrieve, update and
|
|
|
|
partial_update.
|
2015-06-29 12:08:15 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = ConfigAccessPermissions()
|
2017-08-22 14:17:20 +02:00
|
|
|
queryset = ConfigStore.objects.all()
|
2015-06-29 12:08:15 +02:00
|
|
|
|
2019-05-27 18:38:43 +02:00
|
|
|
can_manage_config = None
|
|
|
|
can_manage_logos_and_fonts = None
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("partial_update", "update"):
|
2019-05-27 18:38:43 +02:00
|
|
|
result = self.check_config_permission(self.kwargs["pk"])
|
|
|
|
elif self.action == "reset_groups":
|
|
|
|
result = has_perm(self.request.user, "core.can_manage_config")
|
|
|
|
elif self.action == "bulk_update":
|
|
|
|
result = True # will be checked in the view
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
2019-05-27 18:38:43 +02:00
|
|
|
def check_config_permission(self, key):
|
|
|
|
"""
|
|
|
|
Checks the permissions for one config key.
|
|
|
|
Users needs 'core.can_manage_logos_and_fonts' for all config values starting
|
|
|
|
with 'logo' and 'font'. For all other config values the user needs the default
|
|
|
|
permissions 'core.can_manage_config'.
|
|
|
|
The result is cached for one request to reduce has_perm queries in e.g. bulk updates.
|
|
|
|
"""
|
|
|
|
if key.startswith("logo") or key.startswith("font"):
|
|
|
|
if self.can_manage_logos_and_fonts is None:
|
|
|
|
self.can_manage_logos_and_fonts = has_perm(
|
|
|
|
self.request.user, "core.can_manage_logos_and_fonts"
|
|
|
|
)
|
|
|
|
return self.can_manage_logos_and_fonts
|
|
|
|
else:
|
|
|
|
if self.can_manage_config is None:
|
|
|
|
self.can_manage_config = has_perm(
|
|
|
|
self.request.user, "core.can_manage_config"
|
|
|
|
)
|
|
|
|
return self.can_manage_config
|
|
|
|
|
2015-06-29 12:08:15 +02:00
|
|
|
def update(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Updates a config variable. Only managers can do this.
|
|
|
|
|
|
|
|
Example: {"value": 42}
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
key = kwargs["pk"]
|
|
|
|
value = request.data.get("value")
|
2015-06-29 12:08:15 +02:00
|
|
|
|
|
|
|
# Validate and change value.
|
|
|
|
try:
|
|
|
|
config[key] = value
|
|
|
|
except ConfigNotFound:
|
|
|
|
raise Http404
|
2019-01-18 19:11:22 +01:00
|
|
|
except ConfigError as err:
|
|
|
|
raise ValidationError({"detail": str(err)})
|
2015-06-29 12:08:15 +02:00
|
|
|
|
|
|
|
# Return response.
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response({"key": key, "value": value})
|
2015-07-01 17:48:41 +02:00
|
|
|
|
2019-05-27 18:38:43 +02:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_update(self, request):
|
|
|
|
"""
|
|
|
|
Updates many config variables:
|
|
|
|
[{key: <key>, value: <value>}, ...]
|
|
|
|
"""
|
|
|
|
if not isinstance(request.data, list):
|
|
|
|
raise ValidationError({"detail": "The data needs to be a list"})
|
|
|
|
|
|
|
|
for entry in request.data:
|
|
|
|
key = entry.get("key")
|
|
|
|
if not isinstance(key, str):
|
|
|
|
raise ValidationError({"detail": "The key must be a string."})
|
|
|
|
if not config.exists(key):
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "The key {0} does not exist.", "args": [key]}
|
|
|
|
)
|
|
|
|
if not self.check_config_permission(key):
|
|
|
|
self.permission_denied(request, message=key)
|
|
|
|
if "value" not in entry:
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Invalid input. Config value is missing."}
|
|
|
|
)
|
|
|
|
|
|
|
|
errors = {}
|
|
|
|
for entry in request.data:
|
|
|
|
try:
|
|
|
|
config[entry["key"]] = entry["value"]
|
|
|
|
except ConfigError as err:
|
|
|
|
errors[entry["key"]] = str(err)
|
|
|
|
|
|
|
|
return Response({"errors": errors})
|
|
|
|
|
|
|
|
@list_route(methods=["post"])
|
|
|
|
def reset_groups(self, request):
|
|
|
|
"""
|
|
|
|
Resets multiple groups. The request data contains all
|
|
|
|
(main) group names: [<group1>, ...]
|
|
|
|
"""
|
|
|
|
if not isinstance(request.data, list):
|
|
|
|
raise ValidationError("The data must be a list")
|
|
|
|
for group in request.data:
|
|
|
|
if not isinstance(group, str):
|
|
|
|
raise ValidationError("Every group must be a string")
|
|
|
|
|
|
|
|
for key, config_variable in config.config_variables.items():
|
|
|
|
if (
|
|
|
|
config_variable.group in request.data
|
|
|
|
and config[key] != config_variable.default_value
|
|
|
|
):
|
|
|
|
config[key] = config_variable.default_value
|
|
|
|
|
|
|
|
return Response()
|
|
|
|
|
2015-07-01 17:48:41 +02:00
|
|
|
|
2016-10-21 11:05:24 +02:00
|
|
|
class ProjectorMessageViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for messages.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: list, retrieve, create, update,
|
|
|
|
partial_update and destroy.
|
2016-10-21 11:05:24 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-10-21 11:05:24 +02:00
|
|
|
access_permissions = ProjectorMessageAccessPermissions()
|
|
|
|
queryset = ProjectorMessage.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-10-21 11:05:24 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
2016-10-21 11:05:24 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
class CountdownViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for Countdown.
|
|
|
|
|
2017-02-27 15:37:01 +01:00
|
|
|
There are the following views: list, retrieve, create, update,
|
|
|
|
partial_update and destroy.
|
2016-10-21 11:05:24 +02:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-10-21 11:05:24 +02:00
|
|
|
access_permissions = CountdownAccessPermissions()
|
|
|
|
queryset = Countdown.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-10-21 11:05:24 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
|
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
2016-10-21 11:05:24 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Special API views
|
2015-07-01 17:48:41 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2015-09-24 21:28:30 +02:00
|
|
|
class ServerTime(utils_views.APIView):
|
|
|
|
"""
|
|
|
|
Returns the server time as UNIX timestamp.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["get"]
|
2015-09-24 21:28:30 +02:00
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
|
|
|
return now().timestamp()
|
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
class VersionView(utils_views.APIView):
|
|
|
|
"""
|
|
|
|
Returns a dictionary with the OpenSlides version and the version of all
|
|
|
|
plugins.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["get"]
|
2015-07-01 23:18:48 +02:00
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
2019-01-19 17:42:18 +01:00
|
|
|
result: Dict[str, Any] = {
|
|
|
|
"openslides_version": version,
|
|
|
|
"openslides_license": license,
|
|
|
|
"openslides_url": url,
|
|
|
|
"plugins": [],
|
|
|
|
"no_name_yet_users": User.objects.filter(last_login__isnull=False).count(),
|
|
|
|
}
|
2015-07-01 23:18:48 +02:00
|
|
|
# Versions of plugins.
|
|
|
|
for plugin in settings.INSTALLED_PLUGINS:
|
2019-01-06 16:22:33 +01:00
|
|
|
result["plugins"].append(
|
|
|
|
{
|
|
|
|
"verbose_name": get_plugin_verbose_name(plugin),
|
|
|
|
"description": get_plugin_description(plugin),
|
|
|
|
"version": get_plugin_version(plugin),
|
|
|
|
"license": get_plugin_license(plugin),
|
|
|
|
"url": get_plugin_url(plugin),
|
|
|
|
}
|
|
|
|
)
|
2015-07-01 23:18:48 +02:00
|
|
|
return result
|
2018-11-04 14:02:30 +01:00
|
|
|
|
|
|
|
|
2019-05-30 12:50:28 +02:00
|
|
|
class HistoryInformationView(utils_views.APIView):
|
|
|
|
"""
|
|
|
|
View to retrieve information about OpenSlides history.
|
|
|
|
|
|
|
|
Use GET to search history information. The query parameter 'type' determines
|
|
|
|
the type of your search:
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
|
|
/?type=element&value=motions%2Fmotion%3A42 if your search for motion 42
|
|
|
|
|
|
|
|
Use DELETE to clear the history.
|
|
|
|
"""
|
|
|
|
|
|
|
|
http_method_names = ["get", "delete"]
|
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
|
|
|
"""
|
|
|
|
Checks permission and parses query parameters.
|
|
|
|
"""
|
2019-06-17 13:45:16 +02:00
|
|
|
if not has_perm(self.request.user, "core.can_see_history"):
|
2019-05-30 12:50:28 +02:00
|
|
|
self.permission_denied(self.request)
|
|
|
|
type = self.request.query_params.get("type")
|
|
|
|
value = self.request.query_params.get("value")
|
2019-06-12 14:32:08 +02:00
|
|
|
if type not in ("element"):
|
2019-05-30 12:50:28 +02:00
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "Invalid input. Type should be 'element' or 'text'."}
|
|
|
|
)
|
2019-06-12 14:32:08 +02:00
|
|
|
# We currently just support searching by element id.
|
|
|
|
data = self.get_data_element_search(value)
|
2019-05-30 12:50:28 +02:00
|
|
|
return data
|
|
|
|
|
|
|
|
def get_data_element_search(self, value):
|
|
|
|
"""
|
|
|
|
Retrieves history information for element search.
|
|
|
|
"""
|
|
|
|
data = []
|
2019-06-12 14:32:08 +02:00
|
|
|
for instance in History.objects.filter(element_id=value).order_by("-now"):
|
2019-07-23 14:58:01 +02:00
|
|
|
if instance.information:
|
|
|
|
data.append(
|
|
|
|
{
|
|
|
|
"element_id": instance.element_id,
|
|
|
|
"timestamp": instance.now.timestamp(),
|
|
|
|
"information": instance.information,
|
|
|
|
"user_id": instance.user.pk if instance.user else None,
|
|
|
|
}
|
|
|
|
)
|
2019-05-30 12:50:28 +02:00
|
|
|
return data
|
|
|
|
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Deletes and rebuilds the history.
|
|
|
|
"""
|
|
|
|
# Check permission
|
|
|
|
if not in_some_groups(request.user.pk or 0, [GROUP_ADMIN_PK]):
|
|
|
|
self.permission_denied(request)
|
|
|
|
|
|
|
|
# Delete history data and history (via CASCADE)
|
|
|
|
HistoryData.objects.all().delete()
|
|
|
|
|
|
|
|
# Rebuild history.
|
|
|
|
History.objects.build_history()
|
|
|
|
|
|
|
|
return Response({"detail": "History was deleted and rebuild successfully."})
|
|
|
|
|
|
|
|
|
|
|
|
class HistoryDataView(utils_views.APIView):
|
2018-11-04 14:02:30 +01:00
|
|
|
"""
|
|
|
|
View to retrieve the history data of OpenSlides.
|
|
|
|
|
|
|
|
Use query paramter timestamp (UNIX timestamp) to get all elements from begin
|
|
|
|
until (including) this timestamp.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["get"]
|
2018-11-04 14:02:30 +01:00
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
|
|
|
"""
|
2019-07-23 14:58:01 +02:00
|
|
|
Checks if user is in admin group. If yes, all history data until
|
|
|
|
(including) timestamp are collected to build a valid dataset for the client.
|
2018-11-04 14:02:30 +01:00
|
|
|
"""
|
|
|
|
if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]):
|
|
|
|
self.permission_denied(self.request)
|
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
timestamp = int(self.request.query_params.get("timestamp", 0))
|
2019-01-12 23:01:42 +01:00
|
|
|
except ValueError:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-07-23 14:58:01 +02:00
|
|
|
{"detail": "Invalid input. Timestamp should be an integer."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
|
|
|
queryset = History.objects.select_related("full_data")
|
2018-11-04 14:02:30 +01:00
|
|
|
if timestamp:
|
2019-01-06 16:22:33 +01:00
|
|
|
queryset = queryset.filter(
|
|
|
|
now__lte=datetime.datetime.fromtimestamp(timestamp)
|
|
|
|
)
|
2019-07-23 14:58:01 +02:00
|
|
|
|
|
|
|
# collection <--> id <--> full_data
|
|
|
|
dataset: Dict[str, Dict[int, Any]] = defaultdict(dict)
|
2018-11-04 14:02:30 +01:00
|
|
|
for instance in queryset:
|
2019-07-23 14:58:01 +02:00
|
|
|
collection, id = split_element_id(instance.element_id)
|
|
|
|
full_data = instance.full_data.full_data
|
|
|
|
if full_data:
|
|
|
|
dataset[collection][id] = full_data
|
2019-08-01 11:47:09 +02:00
|
|
|
elif id in dataset[collection]:
|
2019-07-23 14:58:01 +02:00
|
|
|
del dataset[collection][id]
|
|
|
|
|
2019-08-01 11:47:09 +02:00
|
|
|
# Ensure, that newer configs than the requested timepoint are also
|
|
|
|
# included, so the client is happy and doesn't miss any config variables.
|
|
|
|
all_current_config_keys = set(config.config_variables.keys())
|
|
|
|
all_old_config_keys = set(
|
|
|
|
map(lambda config: config["key"], dataset["core/config"].values())
|
|
|
|
)
|
|
|
|
missing_keys = all_current_config_keys - all_old_config_keys
|
|
|
|
if missing_keys:
|
2019-07-29 15:19:59 +02:00
|
|
|
config_full_data = async_to_sync(element_cache.get_collection_data)(
|
2019-08-01 11:47:09 +02:00
|
|
|
"core/config"
|
|
|
|
)
|
|
|
|
key_to_id = config.get_key_to_id()
|
|
|
|
for key in missing_keys:
|
|
|
|
id = key_to_id[key]
|
|
|
|
dataset["core/config"][id] = config_full_data[id]
|
|
|
|
|
2019-07-23 14:58:01 +02:00
|
|
|
return {
|
|
|
|
collection: list(dataset[collection].values())
|
|
|
|
for collection in dataset.keys()
|
|
|
|
}
|