Refactored OpenSlides history (HistoryInformation is not a root rest element anymore.).
This commit is contained in:
parent
35c8dc97f5
commit
5a5bc77e62
@ -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].
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
22
openslides/core/migrations/0024_auto_20190605_1105.py
Normal file
22
openslides/core/migrations/0024_auto_20190605_1105.py
Normal 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,
|
||||
),
|
||||
)
|
||||
]
|
@ -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)
|
||||
|
@ -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",)
|
||||
|
@ -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"),
|
||||
]
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user