OpenSlides/openslides/core/views.py

630 lines
21 KiB
Python
Raw Normal View History

import datetime
2018-10-13 08:41:51 +02:00
import os
2019-01-19 17:42:18 +01:00
from typing import Any, Dict
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
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
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
2016-11-08 21:29:26 +01:00
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
2016-10-17 16:56:19 +02:00
from ..utils.plugins import (
2015-06-18 21:48:20 +02:00
get_plugin_description,
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 (
GenericViewSet,
ListModelMixin,
ModelViewSet,
Response,
RetrieveModelMixin,
ValidationError,
detail_route,
2016-10-17 16:56:19 +02:00
list_route,
)
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions,
)
from .config import config
from .exceptions import ConfigError, ConfigNotFound
from .models import (
ChatMessage,
ConfigStore,
Countdown,
History,
HistoryData,
ProjectionDefault,
Projector,
ProjectorMessage,
Tag,
)
# 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.
"""
2018-10-13 08:41:51 +02:00
cache: Dict[str, str] = {}
"""
2018-10-13 08:41:51 +02:00
Saves the path to the index.html.
2018-10-13 08:41:51 +02:00
May be extended later to cache every template.
"""
2017-09-26 14:19:48 +02:00
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
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:
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
# Viewsets for the REST API
2019-01-06 16:22:33 +01:00
2016-09-12 11:05:34 +02:00
class ProjectorViewSet(ModelViewSet):
"""
API endpoint for the projector slide info.
There are the following views: See strings in check_view_permissions().
"""
2019-01-06 16:22:33 +01:00
access_permissions = ProjectorAccessPermissions()
queryset = Projector.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"):
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")
elif self.action in (
2019-01-06 16:22:33 +01:00
"create",
"update",
"partial_update",
"destroy",
"control_view",
"set_scroll",
"set_projectiondefault",
"project",
):
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"
)
else:
result = False
return result
2016-09-12 11:05:34 +02:00
def destroy(self, *args, **kwargs):
"""
REST API operation for DELETE requests.
Assigns all ProjectionDefault objects from this projector to the
first projector found.
"""
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()
new_projector_id = (
Projector.objects.exclude(pk=projector_instance.pk).first().pk
)
for projection_default in ProjectionDefault.objects.all():
if projection_default.projector.id == projector_instance.id:
projection_default.projector_id = new_projector_id
projection_default.save()
2016-09-12 11:05:34 +02:00
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
@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.
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.
"""
projector = self.get_object()
elements = request.data.get("elements")
preview = request.data.get("preview")
history_element = request.data.get("append_to_history")
delete_last_history_element = request.data.get(
"delete_last_history_element", False
)
changed_data = {}
if elements is not None:
changed_data["elements"] = elements
if preview is not None:
changed_data["elements_preview"] = preview
if history_element is not None and delete_last_history_element is False:
history = projector.elements_history + [history_element]
changed_data["elements_history"] = history
if history_element is None and delete_last_history_element is True:
history = projector.elements_history[:-1]
changed_data["elements_history"] = history
serializer = self.get_serializer(projector, data=changed_data, partial=True)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response()
2019-01-06 16:22:33 +01:00
@detail_route(methods=["post"])
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.
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')."
}
)
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
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
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)
return Response()
2019-01-06 16:22:33 +01:00
@detail_route(methods=["post"])
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."})
projector_instance = self.get_object()
projector_instance.scroll = request.data
projector_instance.save()
2019-01-12 23:01:42 +01:00
message = f"Setting scroll to {request.data} was successful."
2019-01-06 16:22:33 +01:00
return Response({"detail": message})
2016-09-12 11:05:34 +02:00
class ProjectionDefaultViewSet(ModelViewSet):
"""
API endpoint for projection defaults.
2016-09-12 11:05:34 +02:00
There are the following views: list, retrieve, create, update,
partial_update and destroy.
"""
2016-09-12 11:05:34 +02: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)
elif self.action in ("create", "partial_update", "update", "destroy"):
result = has_perm(self.request.user, "core.can_manage_projector")
else:
result = False
return result
2016-09-12 11:05:34 +02:00
class TagViewSet(ModelViewSet):
"""
API endpoint for tags.
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
2019-01-06 16:22:33 +01:00
access_permissions = TagAccessPermissions()
queryset = Tag.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"):
result = self.get_access_permissions().check_permissions(self.request.user)
2019-01-06 16:22:33 +01:00
elif self.action == "metadata":
# Every authenticated user can see the metadata.
# Anonymous users can do so if they are enabled.
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")
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
"""
API endpoint for the config.
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
access_permissions = ConfigAccessPermissions()
2017-08-22 14:17:20 +02:00
queryset = ConfigStore.objects.all()
2015-06-29 12:08:15 +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"):
result = self.get_access_permissions().check_permissions(self.request.user)
2019-01-06 16:22:33 +01:00
elif self.action == "metadata":
# Every authenticated user can see the metadata and list or
# retrieve the config. Anonymous users can do so if they are
# enabled.
result = self.request.user.is_authenticated or anonymous_is_enabled()
2019-01-06 16:22:33 +01:00
elif self.action in ("partial_update", "update"):
2018-01-30 16:12:02 +01:00
# The user needs 'core.can_manage_logos_and_fonts' for all config values
# starting with 'logo' and 'font'. For all other config values th euser needs
2017-03-31 13:48:41 +02:00
# the default permissions 'core.can_manage_config'.
2019-01-06 16:22:33 +01:00
pk = self.kwargs["pk"]
if pk.startswith("logo") or pk.startswith("font"):
result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
2017-03-31 13:48:41 +02:00
else:
2019-01-06 16:22:33 +01:00
result = has_perm(self.request.user, "core.can_manage_config")
else:
result = False
return result
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")
if value is None:
2019-01-06 16:22:33 +01:00
raise ValidationError({"detail": "Invalid input. Config value is missing."})
2015-06-29 12:08:15 +02:00
# Validate and change value.
try:
config[key] = value
except ConfigNotFound:
raise Http404
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})
class ChatMessageViewSet(ModelViewSet):
"""
API endpoint for chat messages.
There are the following views: metadata, list, retrieve and create.
The views partial_update, update and destroy are disabled.
"""
2019-01-06 16:22:33 +01:00
access_permissions = ChatMessageAccessPermissions()
queryset = ChatMessage.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"):
result = self.get_access_permissions().check_permissions(self.request.user)
2019-01-06 16:22:33 +01:00
elif self.action in ("metadata", "create"):
# We do not want anonymous users to use the chat even the anonymous
# group has the permission core.can_use_chat.
2019-01-06 16:22:33 +01:00
result = self.request.user.is_authenticated and has_perm(
self.request.user, "core.can_use_chat"
)
elif self.action == "clear":
result = has_perm(self.request.user, "core.can_use_chat") and has_perm(
self.request.user, "core.can_manage_chat"
)
2016-10-17 16:56:19 +02:00
else:
result = False
return result
def perform_create(self, serializer):
"""
Customized method to inject the request.user into serializer's save
method so that the request.user can be saved into the model field.
"""
serializer.save(user=self.request.user)
# Send chatter via autoupdate because users without permission
# to see users may not have it but can get it now.
inform_changed_data([self.request.user])
2019-01-06 16:22:33 +01:00
@list_route(methods=["post"])
2016-10-17 16:56:19 +02:00
def clear(self, request):
"""
Deletes all chat messages.
"""
# Collect all chat messages with their collection_string and id
chatmessages = ChatMessage.objects.all()
args = []
for chatmessage in chatmessages:
args.append((chatmessage.get_collection_string(), chatmessage.pk))
2016-10-17 16:56:19 +02:00
chatmessages.delete()
# Trigger autoupdate and setup response.
2019-01-12 23:01:42 +01:00
if args:
inform_deleted_data(args)
2019-01-12 23:01:42 +01:00
return Response({"detail": "All chat messages deleted successfully."})
2016-10-17 16:56:19 +02:00
class ProjectorMessageViewSet(ModelViewSet):
"""
API endpoint for messages.
There are the following views: list, retrieve, create, update,
partial_update and destroy.
"""
2019-01-06 16:22:33 +01: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"):
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")
else:
result = False
return result
class CountdownViewSet(ModelViewSet):
"""
API endpoint for Countdown.
There are the following views: list, retrieve, create, update,
partial_update and destroy.
"""
2019-01-06 16:22:33 +01: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"):
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")
else:
result = False
return result
class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
"""
API endpoint for History.
There are the following views: list, retrieve, clear_history.
"""
2019-01-06 16:22:33 +01:00
access_permissions = HistoryAccessPermissions()
queryset = History.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)
elif self.action == "clear_history":
result = in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK])
else:
result = False
return result
2019-01-06 16:22:33 +01:00
@list_route(methods=["post"])
def clear_history(self, request):
"""
Deletes and rebuilds the history.
"""
# Collect all history objects with their collection_string and id.
args = []
for history_obj in History.objects.all():
args.append((history_obj.get_collection_string(), history_obj.pk))
# Delete history data and history (via CASCADE)
HistoryData.objects.all().delete()
# Trigger autoupdate.
2019-01-12 23:01:42 +01:00
if args:
inform_deleted_data(args)
# Rebuild history.
history_instances = History.objects.build_history()
inform_changed_data(history_instances)
# Setup response.
2019-01-12 23:01:42 +01:00
return Response({"detail": "History was deleted successfully."})
# Special API views
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()
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"]
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(),
}
# 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),
}
)
return result
class HistoryView(utils_views.APIView):
"""
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"]
def get_context_data(self, **context):
"""
Checks if user is in admin group. If yes all history data until
(including) timestamp are added to the response data.
"""
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(
{"detail": "Invalid input. Timestamp should be an integer."}
)
data = []
2019-01-06 16:22:33 +01:00
queryset = History.objects.select_related("full_data")
if timestamp:
2019-01-06 16:22:33 +01:00
queryset = queryset.filter(
now__lte=datetime.datetime.fromtimestamp(timestamp)
)
for instance in queryset:
2019-01-06 16:22:33 +01:00
data.append(
{
"full_data": instance.full_data.full_data,
"element_id": instance.element_id,
"timestamp": instance.now.timestamp(),
"information": instance.information,
"user_id": instance.user.pk if instance.user else None,
}
)
return data