Refactored OpenSlides history (HistoryInformation is not a root rest element anymore.).

This commit is contained in:
Norman Jäckel 2019-05-30 12:50:28 +02:00 committed by FinnStutzenstein
parent 35c8dc97f5
commit 5a5bc77e62
9 changed files with 115 additions and 116 deletions

View File

@ -21,7 +21,7 @@ Core:
- Add a change-id system to get only new elements [#3938]. - Add a change-id system to get only new elements [#3938].
- Switch from Yarn back to npm [#3964]. - Switch from Yarn back to npm [#3964].
- Added password reset link (password reset via email) [#3914, #4199]. - Added password reset link (password reset via email) [#3914, #4199].
- Added global history mode [#3977, #4141, #4369, #4373]. - Added global history mode [#3977, #4141, #4369, #4373, #4767].
- Projector refactoring [4119, #4130]. - Projector refactoring [4119, #4130].
- Fixed logo configuration if logo file is deleted [#4374]. - Fixed logo configuration if logo file is deleted [#4374].

View File

@ -44,11 +44,3 @@ class ConfigAccessPermissions(BaseAccessPermissions):
Access permissions container for the config (ConfigStore and Access permissions container for the config (ConfigStore and
ConfigViewSet). ConfigViewSet).
""" """
class HistoryAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for the Histroy.
"""
base_permission = "core.can_see_history"

View File

@ -29,7 +29,6 @@ class CoreAppConfig(AppConfig):
from .views import ( from .views import (
ConfigViewSet, ConfigViewSet,
CountdownViewSet, CountdownViewSet,
HistoryViewSet,
ProjectorMessageViewSet, ProjectorMessageViewSet,
ProjectorViewSet, ProjectorViewSet,
ProjectionDefaultViewSet, ProjectionDefaultViewSet,
@ -91,9 +90,6 @@ class CoreAppConfig(AppConfig):
router.register( router.register(
self.get_model("Countdown").get_collection_string(), CountdownViewSet self.get_model("Countdown").get_collection_string(), CountdownViewSet
) )
router.register(
self.get_model("History").get_collection_string(), HistoryViewSet
)
if "runserver" in sys.argv or "changeconfig" in sys.argv: if "runserver" in sys.argv or "changeconfig" in sys.argv:
startup() startup()
@ -123,7 +119,6 @@ class CoreAppConfig(AppConfig):
"ProjectorMessage", "ProjectorMessage",
"Countdown", "Countdown",
"ConfigStore", "ConfigStore",
"History",
): ):
yield self.get_model(model_name) yield self.get_model(model_name)

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.1 on 2019-06-05 09:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0023_chyron_colors")]
operations = [
migrations.AlterField(
model_name="history",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
)
]

View File

@ -10,7 +10,6 @@ from ..utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
from .access_permissions import ( from .access_permissions import (
ConfigAccessPermissions, ConfigAccessPermissions,
CountdownAccessPermissions, CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions, ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
ProjectorMessageAccessPermissions, ProjectorMessageAccessPermissions,
@ -260,12 +259,8 @@ class HistoryManager(models.Manager):
instances = [] instances = []
history_time = now() history_time = now()
for element in elements: for element in elements:
if ( if element.get("disable_history"):
element.get("disable_history") # Do not update history if history is disabled.
or element["collection_string"]
== self.model.get_collection_string()
):
# Do not update history for history elements itself or if history is disabled.
continue continue
# HistoryData is not a root rest element so there is no autoupdate and not history saving here. # HistoryData is not a root rest element so there is no autoupdate and not history saving here.
data = HistoryData.objects.create(full_data=element["full_data"]) data = HistoryData.objects.create(full_data=element["full_data"])
@ -279,9 +274,7 @@ class HistoryManager(models.Manager):
user_id=element.get("user_id"), user_id=element.get("user_id"),
full_data=data, full_data=data,
) )
instance.save( instance.save()
skip_autoupdate=True
) # Skip autoupdate and of course history saving.
instances.append(instance) instances.append(instance)
return instances return instances
@ -307,7 +300,7 @@ class HistoryManager(models.Manager):
return instances return instances
class History(RESTModelMixin, models.Model): class History(models.Model):
""" """
Django model to save the history of OpenSlides. Django model to save the history of OpenSlides.
@ -315,8 +308,6 @@ class History(RESTModelMixin, models.Model):
delete a user you may lose the information of the user field here. delete a user you may lose the information of the user field here.
""" """
access_permissions = HistoryAccessPermissions()
objects = HistoryManager() objects = HistoryManager()
element_id = models.CharField(max_length=255) element_id = models.CharField(max_length=255)
@ -328,7 +319,7 @@ class History(RESTModelMixin, models.Model):
restricted = models.BooleanField(default=False) restricted = models.BooleanField(default=False)
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, on_delete=SET_NULL_AND_AUTOUPDATE settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
) )
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE) full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)

View File

@ -12,7 +12,6 @@ from ..utils.validate import validate_html
from .models import ( from .models import (
ConfigStore, ConfigStore,
Countdown, Countdown,
History,
ProjectionDefault, ProjectionDefault,
Projector, Projector,
ProjectorMessage, ProjectorMessage,
@ -173,18 +172,3 @@ class CountdownSerializer(ModelSerializer):
"running", "running",
) )
unique_together = ("title",) unique_together = ("title",)
class HistorySerializer(ModelSerializer):
"""
Serializer for core.models.Countdown objects.
Does not contain full data of history object.
"""
information = JSONSerializerField()
class Meta:
model = History
fields = ("id", "element_id", "now", "information", "restricted", "user")
read_only_fields = ("now",)

View File

@ -6,5 +6,10 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"), url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
url(r"^version/$", views.VersionView.as_view(), name="core_version"), url(r"^version/$", views.VersionView.as_view(), name="core_version"),
url(r"^history/$", views.HistoryView.as_view(), name="core_history"), url(
r"^history/information/$",
views.HistoryInformationView.as_view(),
name="core_history_information",
),
url(r"^history/data/$", views.HistoryDataView.as_view(), name="core_history_data"),
] ]

View File

@ -16,7 +16,7 @@ from ..users.models import User
from ..utils import views as utils_views from ..utils import views as utils_views
from ..utils.arguments import arguments from ..utils.arguments import arguments
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.autoupdate import inform_changed_data
from ..utils.plugins import ( from ..utils.plugins import (
get_plugin_description, get_plugin_description,
get_plugin_license, get_plugin_license,
@ -32,12 +32,10 @@ from ..utils.rest_api import (
RetrieveModelMixin, RetrieveModelMixin,
ValidationError, ValidationError,
detail_route, detail_route,
list_route,
) )
from .access_permissions import ( from .access_permissions import (
ConfigAccessPermissions, ConfigAccessPermissions,
CountdownAccessPermissions, CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions, ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
ProjectorMessageAccessPermissions, ProjectorMessageAccessPermissions,
@ -442,53 +440,6 @@ class CountdownViewSet(ModelViewSet):
return result return result
class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
"""
API endpoint for History.
There are the following views: list, retrieve, clear_history.
"""
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
@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.
if args:
inform_deleted_data(args)
# Rebuild history.
history_instances = History.objects.build_history()
inform_changed_data(history_instances)
# Setup response.
return Response({"detail": "History was deleted successfully."})
# Special API views # Special API views
@ -533,7 +484,84 @@ class VersionView(utils_views.APIView):
return result return result
class HistoryView(utils_views.APIView): 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
/?type=text&value=my%20question
Use DELETE to clear the history.
"""
http_method_names = ["get", "delete"]
def get_context_data(self, **context):
"""
Checks permission and parses query parameters.
"""
if not has_perm(self.request.user, "users.can_see_history"):
self.permission_denied(self.request)
type = self.request.query_params.get("type")
value = self.request.query_params.get("value")
if type not in ("element", "text"):
raise ValidationError(
{"detail": "Invalid input. Type should be 'element' or 'text'."}
)
if type == "element":
data = self.get_data_element_search(value)
else:
# type == "text"
data = self.get_data_text_search(value)
return data
def get_data_element_search(self, value):
"""
Retrieves history information for element search.
"""
data = []
for instance in History.objects.filter(element_id=value):
data.append(
{
"element_id": instance.element_id,
"timestamp": instance.now.timestamp(),
"information": instance.information,
"resticted": instance.restricted,
"user_id": instance.user.pk if instance.user else None,
}
)
return data
def get_data_text_search(self, value):
"""
Retrieves history information for text search.
"""
# TODO: Add results here.
return []
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):
""" """
View to retrieve the history data of OpenSlides. View to retrieve the history data of OpenSlides.

View File

@ -1,4 +1,3 @@
import itertools
import threading import threading
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
@ -234,27 +233,10 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
element["full_data"] = instance.get_full_data() element["full_data"] = instance.get_full_data()
# Save histroy here using sync code. # Save histroy here using sync code.
history_instances = save_history(elements) save_history(elements)
# Convert history instances to Elements.
history_elements: List[Element] = []
for history_instance in history_instances:
history_elements.append(
Element(
id=history_instance.get_rest_pk(),
collection_string=history_instance.get_collection_string(),
full_data=history_instance.get_full_data(),
disable_history=True, # This does not matter because history elements can never be part of the history itself.
)
)
# Chain elements and history elements.
itertools.chain(elements, history_elements)
# Update cache and send autoupdate using async code. # Update cache and send autoupdate using async code.
async_to_sync(async_handle_collection_elements)( async_to_sync(async_handle_collection_elements)(elements)
itertools.chain(elements, history_elements)
)
def save_history(elements: Iterable[Element]) -> Iterable: def save_history(elements: Iterable[Element]) -> Iterable: