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].
- Switch from Yarn back to npm [#3964].
- 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].
- 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
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 (
ConfigViewSet,
CountdownViewSet,
HistoryViewSet,
ProjectorMessageViewSet,
ProjectorViewSet,
ProjectionDefaultViewSet,
@ -91,9 +90,6 @@ class CoreAppConfig(AppConfig):
router.register(
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:
startup()
@ -123,7 +119,6 @@ class CoreAppConfig(AppConfig):
"ProjectorMessage",
"Countdown",
"ConfigStore",
"History",
):
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 (
ConfigAccessPermissions,
CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
@ -260,12 +259,8 @@ class HistoryManager(models.Manager):
instances = []
history_time = now()
for element in elements:
if (
element.get("disable_history")
or element["collection_string"]
== self.model.get_collection_string()
):
# Do not update history for history elements itself or if history is disabled.
if element.get("disable_history"):
# Do not update history if history is disabled.
continue
# 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"])
@ -279,9 +274,7 @@ class HistoryManager(models.Manager):
user_id=element.get("user_id"),
full_data=data,
)
instance.save(
skip_autoupdate=True
) # Skip autoupdate and of course history saving.
instance.save()
instances.append(instance)
return instances
@ -307,7 +300,7 @@ class HistoryManager(models.Manager):
return instances
class History(RESTModelMixin, models.Model):
class History(models.Model):
"""
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.
"""
access_permissions = HistoryAccessPermissions()
objects = HistoryManager()
element_id = models.CharField(max_length=255)
@ -328,7 +319,7 @@ class History(RESTModelMixin, models.Model):
restricted = models.BooleanField(default=False)
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)

View File

@ -12,7 +12,6 @@ from ..utils.validate import validate_html
from .models import (
ConfigStore,
Countdown,
History,
ProjectionDefault,
Projector,
ProjectorMessage,
@ -173,18 +172,3 @@ class CountdownSerializer(ModelSerializer):
"running",
)
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 = [
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
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.arguments import arguments
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 (
get_plugin_description,
get_plugin_license,
@ -32,12 +32,10 @@ from ..utils.rest_api import (
RetrieveModelMixin,
ValidationError,
detail_route,
list_route,
)
from .access_permissions import (
ConfigAccessPermissions,
CountdownAccessPermissions,
HistoryAccessPermissions,
ProjectionDefaultAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
@ -442,53 +440,6 @@ class CountdownViewSet(ModelViewSet):
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
@ -533,7 +484,84 @@ class VersionView(utils_views.APIView):
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 File

@ -1,4 +1,3 @@
import itertools
import threading
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()
# Save histroy here using sync code.
history_instances = 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)
save_history(elements)
# Update cache and send autoupdate using async code.
async_to_sync(async_handle_collection_elements)(
itertools.chain(elements, history_elements)
)
async_to_sync(async_handle_collection_elements)(elements)
def save_history(elements: Iterable[Element]) -> Iterable: