From 5a5bc77e62d20b0344970f7878cf5c1e4bd7306a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Thu, 30 May 2019 12:50:28 +0200 Subject: [PATCH] Refactored OpenSlides history (HistoryInformation is not a root rest element anymore.). --- CHANGELOG.rst | 2 +- openslides/core/access_permissions.py | 8 -- openslides/core/apps.py | 5 - .../migrations/0024_auto_20190605_1105.py | 22 +++ openslides/core/models.py | 19 +-- openslides/core/serializers.py | 16 --- openslides/core/urls.py | 7 +- openslides/core/views.py | 130 +++++++++++------- openslides/utils/autoupdate.py | 22 +-- 9 files changed, 115 insertions(+), 116 deletions(-) create mode 100644 openslides/core/migrations/0024_auto_20190605_1105.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7820ec676..c30db7be4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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]. diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index 3dfbe1769..24e75f836 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -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" diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 5b0c35425..00969e6bf 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -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) diff --git a/openslides/core/migrations/0024_auto_20190605_1105.py b/openslides/core/migrations/0024_auto_20190605_1105.py new file mode 100644 index 000000000..8badfb907 --- /dev/null +++ b/openslides/core/migrations/0024_auto_20190605_1105.py @@ -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, + ), + ) + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 577309885..86b92eba7 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -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) diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index f7f5eae0a..920939009 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -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",) diff --git a/openslides/core/urls.py b/openslides/core/urls.py index e5e887367..6275c582c 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -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"), ] diff --git a/openslides/core/views.py b/openslides/core/views.py index a89e38d70..c875ddfb4 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -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. diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 399e381f7..9a3272078 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -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: