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].
|
- 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].
|
||||||
|
|
||||||
|
@ -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"
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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 (
|
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)
|
||||||
|
@ -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",)
|
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user