Autoupdate on element deletion
Make sure, that a related element gets an autoupdate, when the main object is deleted
This commit is contained in:
parent
a895481cef
commit
11ba7b9841
33
openslides/agenda/migrations/0006_auto_20190119_1425.py
Normal file
33
openslides/agenda/migrations/0006_auto_20190119_1425.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("agenda", "0005_auto_20180815_1109")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="item",
|
||||||
|
name="parent",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
related_name="children",
|
||||||
|
to="agenda.Item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="speaker",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -15,6 +15,7 @@ from openslides.utils.exceptions import OpenSlidesError
|
|||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
|
|
||||||
|
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import ItemAccessPermissions
|
from .access_permissions import ItemAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -233,7 +234,7 @@ class Item(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="children",
|
related_name="children",
|
||||||
@ -374,7 +375,7 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
objects = SpeakerManager()
|
objects = SpeakerManager()
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who speaks.
|
ForeinKey to the user who speaks.
|
||||||
"""
|
"""
|
||||||
|
31
openslides/assignments/migrations/0006_auto_20190119_1425.py
Normal file
31
openslides/assignments/migrations/0006_auto_20190119_1425.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("assignments", "0005_auto_20180822_1042")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentoption",
|
||||||
|
name="candidate",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentrelateduser",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -21,6 +21,7 @@ from openslides.utils.autoupdate import inform_changed_data
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
|
|
||||||
|
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
|||||||
ForeinKey to the assignment.
|
ForeinKey to the assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who is related to the assignment.
|
ForeinKey to the user who is related to the assignment.
|
||||||
"""
|
"""
|
||||||
@ -366,7 +367,9 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
|||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
||||||
)
|
)
|
||||||
candidate = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
candidate = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
|
)
|
||||||
weight = models.IntegerField(default=0)
|
weight = models.IntegerField(default=0)
|
||||||
|
|
||||||
vote_class = AssignmentVote
|
vote_class = AssignmentVote
|
||||||
|
@ -5,14 +5,10 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0010_auto_20190118_1908")]
|
||||||
('core', '0010_auto_20190118_1908'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='history',
|
model_name="history", name="now", field=models.DateTimeField()
|
||||||
name='now',
|
)
|
||||||
field=models.DateTimeField(),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
41
openslides/core/migrations/0012_auto_20190119_1425.py
Normal file
41
openslides/core/migrations/0012_auto_20190119_1425.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0011_auto_20190119_0958")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="chatmessage",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="history",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="projectiondefault",
|
||||||
|
name="projector",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="projectiondefaults",
|
||||||
|
to="core.Projector",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -6,7 +6,11 @@ from jsonfield import JSONField
|
|||||||
|
|
||||||
from ..utils.autoupdate import Element
|
from ..utils.autoupdate import Element
|
||||||
from ..utils.cache import element_cache, get_element_id
|
from ..utils.cache import element_cache, get_element_id
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import (
|
||||||
|
CASCADE_AND_AUTOUODATE,
|
||||||
|
SET_NULL_AND_AUTOUPDATE,
|
||||||
|
RESTModelMixin,
|
||||||
|
)
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ChatMessageAccessPermissions,
|
ChatMessageAccessPermissions,
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
@ -108,7 +112,7 @@ class ProjectionDefault(RESTModelMixin, models.Model):
|
|||||||
display_name = models.CharField(max_length=256)
|
display_name = models.CharField(max_length=256)
|
||||||
|
|
||||||
projector = models.ForeignKey(
|
projector = models.ForeignKey(
|
||||||
Projector, on_delete=models.CASCADE, related_name="projectiondefaults"
|
Projector, on_delete=models.PROTECT, related_name="projectiondefaults"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
@ -179,7 +183,7 @@ class ChatMessage(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
@ -269,7 +273,7 @@ class HistoryManager(models.Manager):
|
|||||||
history_time = now()
|
history_time = now()
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if (
|
if (
|
||||||
element["disable_history"]
|
element.get("disable_history")
|
||||||
or element["collection_string"]
|
or element["collection_string"]
|
||||||
== self.model.get_collection_string()
|
== self.model.get_collection_string()
|
||||||
):
|
):
|
||||||
@ -282,8 +286,8 @@ class HistoryManager(models.Manager):
|
|||||||
element["collection_string"], element["id"]
|
element["collection_string"], element["id"]
|
||||||
),
|
),
|
||||||
now=history_time,
|
now=history_time,
|
||||||
information=element["information"],
|
information=element.get("information", ""),
|
||||||
user_id=element["user_id"],
|
user_id=element.get("user_id"),
|
||||||
full_data=data,
|
full_data=data,
|
||||||
)
|
)
|
||||||
instance.save(
|
instance.save(
|
||||||
@ -308,9 +312,6 @@ class HistoryManager(models.Manager):
|
|||||||
id=full_data["id"],
|
id=full_data["id"],
|
||||||
collection_string=collection_string,
|
collection_string=collection_string,
|
||||||
full_data=full_data,
|
full_data=full_data,
|
||||||
information="",
|
|
||||||
user_id=None,
|
|
||||||
disable_history=False,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
instances = self.add_elements(elements)
|
instances = self.add_elements(elements)
|
||||||
@ -336,7 +337,7 @@ class History(RESTModelMixin, models.Model):
|
|||||||
information = models.CharField(max_length=255)
|
information = models.CharField(max_length=255)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
settings.AUTH_USER_MODEL, null=True, on_delete=SET_NULL_AND_AUTOUPDATE
|
||||||
)
|
)
|
||||||
|
|
||||||
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
||||||
|
351
openslides/core/models.py.orig
Normal file
351
openslides/core/models.py.orig
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
from ..utils.autoupdate import Element
|
||||||
|
from ..utils.cache import element_cache, get_element_id
|
||||||
|
from ..utils.models import (
|
||||||
|
CASCADE_AND_AUTOUODATE,
|
||||||
|
SET_NULL_AND_AUTOUPDATE,
|
||||||
|
RESTModelMixin,
|
||||||
|
)
|
||||||
|
from .access_permissions import (
|
||||||
|
ChatMessageAccessPermissions,
|
||||||
|
ConfigAccessPermissions,
|
||||||
|
CountdownAccessPermissions,
|
||||||
|
HistoryAccessPermissions,
|
||||||
|
ProjectorAccessPermissions,
|
||||||
|
ProjectorMessageAccessPermissions,
|
||||||
|
TagAccessPermissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorManager(models.Manager):
|
||||||
|
"""
|
||||||
|
Customized model manager to support our get_full_queryset method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_full_queryset(self):
|
||||||
|
"""
|
||||||
|
Returns the normal queryset with all projectors. In the background
|
||||||
|
projector defaults are prefetched from the database.
|
||||||
|
"""
|
||||||
|
return self.get_queryset().prefetch_related("projectiondefaults")
|
||||||
|
|
||||||
|
|
||||||
|
class Projector(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for all projectors.
|
||||||
|
|
||||||
|
The elements field contains a list. Every element must have at least the
|
||||||
|
property "name".
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "topics/topic",
|
||||||
|
"id": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core/countdown",
|
||||||
|
"id": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core/clock",
|
||||||
|
"id": 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
If the config field is empty or invalid the projector shows a default
|
||||||
|
slide.
|
||||||
|
|
||||||
|
There are two additional fields to control the behavior of the projector
|
||||||
|
view itself: scale and scroll.
|
||||||
|
|
||||||
|
The projector can be controlled using the REST API with POST requests
|
||||||
|
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = ProjectorAccessPermissions()
|
||||||
|
|
||||||
|
objects = ProjectorManager()
|
||||||
|
|
||||||
|
elements = JSONField()
|
||||||
|
elements_preview = JSONField()
|
||||||
|
elements_history = JSONField()
|
||||||
|
|
||||||
|
scale = models.IntegerField(default=0)
|
||||||
|
scroll = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
width = models.PositiveIntegerField(default=1024)
|
||||||
|
height = models.PositiveIntegerField(default=768)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255, unique=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Contains general permissions that can not be placed in a specific app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_permissions = ()
|
||||||
|
permissions = (
|
||||||
|
("can_see_projector", "Can see the projector"),
|
||||||
|
("can_manage_projector", "Can manage the projector"),
|
||||||
|
("can_see_frontpage", "Can see the front page"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectionDefault(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for the projection defaults like motions, agenda, list of
|
||||||
|
speakers and thelike. The name is the technical name like 'topics' or
|
||||||
|
'motions'. For apps the name should be the app name to get keep the
|
||||||
|
ProjectionDefault for apps generic. But it is possible to give some
|
||||||
|
special name like 'list_of_speakers'. The display_name is the shown
|
||||||
|
name on the front end for the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
display_name = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
projector = models.ForeignKey(
|
||||||
|
Projector, on_delete=models.PROTECT, related_name="projectiondefaults"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_root_rest_element(self):
|
||||||
|
return self.projector
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.display_name
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for tags. This tags can be used for other models like agenda items,
|
||||||
|
motions or assignments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = TagAccessPermissions()
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("name",)
|
||||||
|
default_permissions = ()
|
||||||
|
permissions = (("can_manage_tags", "Can manage tags"),)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigStore(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
A model class to store all config variables in the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = ConfigAccessPermissions()
|
||||||
|
|
||||||
|
key = models.CharField(max_length=255, unique=True, db_index=True)
|
||||||
|
"""A string, the key of the config variable."""
|
||||||
|
|
||||||
|
value = JSONField()
|
||||||
|
"""The value of the config variable. """
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
permissions = (
|
||||||
|
("can_manage_config", "Can manage configuration"),
|
||||||
|
("can_manage_logos_and_fonts", "Can manage logos and fonts"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_collection_string(cls):
|
||||||
|
return "core/config"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for chat messages.
|
||||||
|
|
||||||
|
At the moment we only have one global chat room for managers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = ChatMessageAccessPermissions()
|
||||||
|
can_see_permission = "core.can_use_chat"
|
||||||
|
|
||||||
|
message = models.TextField()
|
||||||
|
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
permissions = (
|
||||||
|
("can_use_chat", "Can use the chat"),
|
||||||
|
("can_manage_chat", "Can manage the chat"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Message {self.timestamp}"
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorMessage(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for ProjectorMessages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = ProjectorMessageAccessPermissions()
|
||||||
|
|
||||||
|
message = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
|
class Countdown(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for countdowns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_permissions = CountdownAccessPermissions()
|
||||||
|
|
||||||
|
description = models.CharField(max_length=256, blank=True)
|
||||||
|
|
||||||
|
running = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
default_time = models.PositiveIntegerField(default=60)
|
||||||
|
|
||||||
|
countdown_time = models.FloatField(default=60)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
def control(self, action, skip_autoupdate=False):
|
||||||
|
if action not in ("start", "stop", "reset"):
|
||||||
|
raise ValueError(
|
||||||
|
f"Action must be 'start', 'stop' or 'reset', not {action}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "start":
|
||||||
|
self.running = True
|
||||||
|
self.countdown_time = now().timestamp() + self.default_time
|
||||||
|
elif action == "stop" and self.running:
|
||||||
|
self.running = False
|
||||||
|
self.countdown_time = self.countdown_time - now().timestamp()
|
||||||
|
else: # reset
|
||||||
|
self.running = False
|
||||||
|
self.countdown_time = self.default_time
|
||||||
|
self.save(skip_autoupdate=skip_autoupdate)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryData(models.Model):
|
||||||
|
"""
|
||||||
|
Django model to save the history of OpenSlides.
|
||||||
|
|
||||||
|
This is not a RESTModel. It is not cachable and can only be reached by a
|
||||||
|
special viewset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
full_data = JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryManager(models.Manager):
|
||||||
|
"""
|
||||||
|
Customized model manager for the history model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_elements(self, elements):
|
||||||
|
"""
|
||||||
|
Method to add elements to the history. This does not trigger autoupdate.
|
||||||
|
"""
|
||||||
|
with transaction.atomic():
|
||||||
|
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.
|
||||||
|
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"])
|
||||||
|
instance = self.model(
|
||||||
|
element_id=get_element_id(
|
||||||
|
element["collection_string"], element["id"]
|
||||||
|
),
|
||||||
|
<<<<<<< HEAD
|
||||||
|
now=history_time,
|
||||||
|
information=element["information"],
|
||||||
|
user_id=element["user_id"],
|
||||||
|
=======
|
||||||
|
information=element.get("information", ""),
|
||||||
|
user_id=element.get("user_id"),
|
||||||
|
>>>>>>> Autoupdate on element deletion
|
||||||
|
full_data=data,
|
||||||
|
)
|
||||||
|
instance.save(
|
||||||
|
skip_autoupdate=True
|
||||||
|
) # Skip autoupdate and of course history saving.
|
||||||
|
instances.append(instance)
|
||||||
|
return instances
|
||||||
|
|
||||||
|
def build_history(self):
|
||||||
|
"""
|
||||||
|
Method to add all cachables to the history.
|
||||||
|
"""
|
||||||
|
# TODO: Add lock to prevent multiple history builds at once. See #4039.
|
||||||
|
instances = None
|
||||||
|
if self.all().count() == 0:
|
||||||
|
elements = []
|
||||||
|
all_full_data = async_to_sync(element_cache.get_all_full_data)()
|
||||||
|
for collection_string, data in all_full_data.items():
|
||||||
|
for full_data in data:
|
||||||
|
elements.append(
|
||||||
|
Element(
|
||||||
|
id=full_data["id"],
|
||||||
|
collection_string=collection_string,
|
||||||
|
full_data=full_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
instances = self.add_elements(elements)
|
||||||
|
return instances
|
||||||
|
|
||||||
|
|
||||||
|
class History(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Django model to save the history of OpenSlides.
|
||||||
|
|
||||||
|
This model itself is not part of the history. This means that if you
|
||||||
|
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)
|
||||||
|
|
||||||
|
now = models.DateTimeField()
|
||||||
|
|
||||||
|
information = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, null=True, on_delete=SET_NULL_AND_AUTOUPDATE
|
||||||
|
)
|
||||||
|
|
||||||
|
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
23
openslides/mediafiles/migrations/0003_auto_20190119_1425.py
Normal file
23
openslides/mediafiles/migrations/0003_auto_20190119_1425.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("mediafiles", "0002_mediafile_private")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="uploader",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
@ -3,7 +3,7 @@ from django.db import models
|
|||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
|
||||||
from .access_permissions import MediafileAccessPermissions
|
from .access_permissions import MediafileAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
"""A string representing the title of the file."""
|
"""A string representing the title of the file."""
|
||||||
|
|
||||||
uploader = models.ForeignKey(
|
uploader = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
"""A user – the uploader of a file."""
|
"""A user – the uploader of a file."""
|
||||||
|
|
||||||
|
@ -6,20 +6,20 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0018_auto_20190118_2101")]
|
||||||
('motions', '0018_auto_20190118_2101'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='created',
|
name="created",
|
||||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='last_modified',
|
name="last_modified",
|
||||||
field=models.DateTimeField(auto_now=True),
|
field=models.DateTimeField(auto_now=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
123
openslides/motions/migrations/0020_auto_20190119_1425.py
Normal file
123
openslides/motions/migrations/0020_auto_20190119_1425.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("motions", "0019_auto_20190119_1025")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="category",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to="motions.Category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="motion_block",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to="motions.MotionBlock",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="parent",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
related_name="amendments",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="recommendation",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
related_name="+",
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="sort_parent",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
related_name="children",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motion",
|
||||||
|
name="statute_paragraph",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
related_name="motions",
|
||||||
|
to="motions.StatuteParagraph",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionchangerecommendation",
|
||||||
|
name="author",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionchangerecommendation",
|
||||||
|
name="motion",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
related_name="change_recommendations",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionlog",
|
||||||
|
name="person",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="submitter",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflow",
|
||||||
|
name="first_state",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -23,6 +23,7 @@ from openslides.utils.autoupdate import inform_changed_data
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
|
|
||||||
|
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
@ -141,7 +142,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
recommendation = models.ForeignKey(
|
recommendation = models.ForeignKey(
|
||||||
"State", related_name="+", on_delete=models.SET_NULL, null=True
|
"State", related_name="+", on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
The recommendation of a person or committee for this motion.
|
The recommendation of a person or committee for this motion.
|
||||||
@ -171,7 +172,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
sort_parent = models.ForeignKey(
|
sort_parent = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="children",
|
related_name="children",
|
||||||
@ -181,14 +182,14 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
"Category", on_delete=models.SET_NULL, null=True, blank=True
|
"Category", on_delete=SET_NULL_AND_AUTOUPDATE, null=True, blank=True
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
ForeignKey to one category of motions.
|
ForeignKey to one category of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
motion_block = models.ForeignKey(
|
motion_block = models.ForeignKey(
|
||||||
"MotionBlock", on_delete=models.SET_NULL, null=True, blank=True
|
"MotionBlock", on_delete=SET_NULL_AND_AUTOUPDATE, null=True, blank=True
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
ForeignKey to one block of motions.
|
ForeignKey to one block of motions.
|
||||||
@ -207,7 +208,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="amendments",
|
related_name="amendments",
|
||||||
@ -220,7 +221,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
statute_paragraph = models.ForeignKey(
|
statute_paragraph = models.ForeignKey(
|
||||||
StatuteParagraph,
|
StatuteParagraph,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="motions",
|
related_name="motions",
|
||||||
@ -704,7 +705,7 @@ class Submitter(RESTModelMixin, models.Model):
|
|||||||
Use custom Manager.
|
Use custom Manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
"""
|
"""
|
||||||
ForeignKey to the user who is the submitter.
|
ForeignKey to the user who is the submitter.
|
||||||
"""
|
"""
|
||||||
@ -754,7 +755,7 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
|||||||
objects = MotionChangeRecommendationManager()
|
objects = MotionChangeRecommendationManager()
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion, on_delete=models.CASCADE, related_name="change_recommendations"
|
Motion, on_delete=CASCADE_AND_AUTOUODATE, related_name="change_recommendations"
|
||||||
)
|
)
|
||||||
"""The motion to which the change recommendation belongs."""
|
"""The motion to which the change recommendation belongs."""
|
||||||
|
|
||||||
@ -780,7 +781,7 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
|||||||
"""The replacement for the section of the original text specified by motion, line_from and line_to"""
|
"""The replacement for the section of the original text specified by motion, line_from and line_to"""
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
"""A user object, who created this change recommendation. Optional."""
|
"""A user object, who created this change recommendation. Optional."""
|
||||||
|
|
||||||
@ -921,7 +922,7 @@ class MotionLog(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
person = models.ForeignKey(
|
person = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
||||||
)
|
)
|
||||||
"""A user object, who created the log message. Optional."""
|
"""A user object, who created the log message. Optional."""
|
||||||
|
|
||||||
@ -1192,7 +1193,7 @@ class Workflow(RESTModelMixin, models.Model):
|
|||||||
"""A string representing the workflow."""
|
"""A string representing the workflow."""
|
||||||
|
|
||||||
first_state = models.OneToOneField(
|
first_state = models.OneToOneField(
|
||||||
State, on_delete=models.SET_NULL, related_name="+", null=True, blank=True
|
State, on_delete=models.CASCADE, related_name="+", null=True
|
||||||
)
|
)
|
||||||
"""A one-to-one relation to a state, the starting point for the workflow."""
|
"""A one-to-one relation to a state, the starting point for the workflow."""
|
||||||
|
|
||||||
|
@ -569,7 +569,11 @@ class MotionViewSet(ModelViewSet):
|
|||||||
person=request.user,
|
person=request.user,
|
||||||
skip_autoupdate=True,
|
skip_autoupdate=True,
|
||||||
)
|
)
|
||||||
inform_changed_data(motion, information=f"State set to {motion.state.name}.", user_id=request.user.pk)
|
inform_changed_data(
|
||||||
|
motion,
|
||||||
|
information=f"State set to {motion.state.name}.",
|
||||||
|
user_id=request.user.pk,
|
||||||
|
)
|
||||||
return Response({"detail": message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@list_route(methods=["post"])
|
@list_route(methods=["post"])
|
||||||
@ -1348,9 +1352,8 @@ class StateViewSet(
|
|||||||
Customized view endpoint to delete a state.
|
Customized view endpoint to delete a state.
|
||||||
"""
|
"""
|
||||||
state = self.get_object()
|
state = self.get_object()
|
||||||
if (
|
if state.workflow.first_state.pk == state.pk:
|
||||||
state.workflow.first_state.pk == state.pk
|
# is this the first state of the workflow?
|
||||||
): # is this the first state of the workflow?
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "You cannot delete the first state of the workflow."}
|
{"detail": "You cannot delete the first state of the workflow."}
|
||||||
)
|
)
|
||||||
|
22
openslides/users/migrations/0009_auto_20190119_1425.py
Normal file
22
openslides/users/migrations/0009_auto_20190119_1425.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-19 13:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("users", "0008_user_gender")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="personalnote",
|
||||||
|
name="user",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
@ -19,7 +19,7 @@ from jsonfield import JSONField
|
|||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.auth import GROUP_ADMIN_PK
|
from ..utils.auth import GROUP_ADMIN_PK
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import CASCADE_AND_AUTOUODATE, RESTModelMixin
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
GroupAccessPermissions,
|
GroupAccessPermissions,
|
||||||
PersonalNoteAccessPermissions,
|
PersonalNoteAccessPermissions,
|
||||||
@ -330,7 +330,7 @@ class PersonalNote(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
objects = PersonalNoteManager()
|
objects = PersonalNoteManager()
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUODATE)
|
||||||
notes = JSONField()
|
notes = JSONField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -369,8 +369,6 @@ class GroupViewSet(ModelViewSet):
|
|||||||
id=full_data["id"],
|
id=full_data["id"],
|
||||||
collection_string=cachable.get_collection_string(),
|
collection_string=cachable.get_collection_string(),
|
||||||
full_data=full_data,
|
full_data=full_data,
|
||||||
information="",
|
|
||||||
user_id=None,
|
|
||||||
disable_history=True,
|
disable_history=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -9,19 +9,21 @@ from mypy_extensions import TypedDict
|
|||||||
|
|
||||||
from .cache import element_cache, get_element_id
|
from .cache import element_cache, get_element_id
|
||||||
from .projector import get_projectot_data
|
from .projector import get_projectot_data
|
||||||
|
from .utils import get_model_from_collection_string
|
||||||
|
|
||||||
|
|
||||||
Element = TypedDict(
|
class ElementBase(TypedDict):
|
||||||
"Element",
|
id: int
|
||||||
{
|
collection_string: str
|
||||||
"id": int,
|
full_data: Optional[Dict[str, Any]]
|
||||||
"collection_string": str,
|
|
||||||
"full_data": Optional[Dict[str, Any]],
|
|
||||||
"information": str,
|
class Element(ElementBase, total=False):
|
||||||
"user_id": Optional[int],
|
information: str
|
||||||
"disable_history": bool,
|
user_id: Optional[int]
|
||||||
},
|
disable_history: bool
|
||||||
)
|
reload: bool
|
||||||
|
|
||||||
|
|
||||||
AutoupdateFormat = TypedDict(
|
AutoupdateFormat = TypedDict(
|
||||||
"AutoupdateFormat",
|
"AutoupdateFormat",
|
||||||
@ -68,7 +70,6 @@ def inform_changed_data(
|
|||||||
full_data=root_instance.get_full_data(),
|
full_data=root_instance.get_full_data(),
|
||||||
information=information,
|
information=information,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
disable_history=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bundle = autoupdate_bundle.get(threading.get_ident())
|
bundle = autoupdate_bundle.get(threading.get_ident())
|
||||||
@ -100,7 +101,6 @@ def inform_deleted_data(
|
|||||||
full_data=None,
|
full_data=None,
|
||||||
information=information,
|
information=information,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
disable_history=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bundle = autoupdate_bundle.get(threading.get_ident())
|
bundle = autoupdate_bundle.get(threading.get_ident())
|
||||||
@ -201,6 +201,12 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if elements:
|
if elements:
|
||||||
|
for element in elements:
|
||||||
|
if element.get("reload"):
|
||||||
|
model = get_model_from_collection_string(element["collection_string"])
|
||||||
|
instance = model.objects.get(pk=element["id"])
|
||||||
|
element["full_data"] = instance.get_full_data()
|
||||||
|
|
||||||
# Save histroy here using sync code.
|
# Save histroy here using sync code.
|
||||||
history_instances = save_history(elements)
|
history_instances = save_history(elements)
|
||||||
|
|
||||||
@ -212,8 +218,6 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
|||||||
id=history_instance.get_rest_pk(),
|
id=history_instance.get_rest_pk(),
|
||||||
collection_string=history_instance.get_collection_string(),
|
collection_string=history_instance.get_collection_string(),
|
||||||
full_data=history_instance.get_full_data(),
|
full_data=history_instance.get_full_data(),
|
||||||
information="",
|
|
||||||
user_id=None,
|
|
||||||
disable_history=True, # This does not matter because history elements can never be part of the history itself.
|
disable_history=True, # This does not matter because history elements can never be part of the history itself.
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -227,9 +231,8 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_history(
|
def save_history(elements: Iterable[Element]) -> Iterable:
|
||||||
elements: Iterable[Element]
|
# TODO: Try to write Iterable[History] here
|
||||||
) -> Iterable: # TODO: Try to write Iterable[History] here
|
|
||||||
"""
|
"""
|
||||||
Thin wrapper around the call of history saving manager method.
|
Thin wrapper around the call of history saving manager method.
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from .access_permissions import BaseAccessPermissions
|
from .access_permissions import BaseAccessPermissions
|
||||||
|
from .autoupdate import Element, inform_changed_data, inform_changed_elements
|
||||||
from .rest_api import model_serializer_classes
|
from .rest_api import model_serializer_classes
|
||||||
from .utils import convert_camel_case_to_pseudo_snake_case
|
from .utils import convert_camel_case_to_pseudo_snake_case
|
||||||
|
|
||||||
@ -153,3 +154,41 @@ class RESTModelMixin:
|
|||||||
__import__(module_name)
|
__import__(module_name)
|
||||||
serializer_class = model_serializer_classes[type(self)]
|
serializer_class = model_serializer_classes[type(self)]
|
||||||
return serializer_class(self).data
|
return serializer_class(self).data
|
||||||
|
|
||||||
|
|
||||||
|
def SET_NULL_AND_AUTOUPDATE(
|
||||||
|
collector: Any, field: Any, sub_objs: Any, using: Any
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Like models.SET_NULL but also informs the autoupdate system about the
|
||||||
|
instance that was reverenced.
|
||||||
|
"""
|
||||||
|
if len(sub_objs) != 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
"SET_NULL_AND_AUTOUPDATE in an invalid usecase. Please open an issue!"
|
||||||
|
)
|
||||||
|
setattr(sub_objs[0], field.name, None)
|
||||||
|
inform_changed_data(sub_objs[0])
|
||||||
|
models.SET_NULL(collector, field, sub_objs, using)
|
||||||
|
|
||||||
|
|
||||||
|
def CASCADE_AND_AUTOUODATE(
|
||||||
|
collector: Any, field: Any, sub_objs: Any, using: Any
|
||||||
|
) -> None:
|
||||||
|
if len(sub_objs) != 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
"CASCADE_AND_AUTOUPDATE in an invalid usecase. Please open an issue!"
|
||||||
|
)
|
||||||
|
|
||||||
|
root_rest_element = sub_objs[0].get_root_rest_element()
|
||||||
|
inform_changed_elements(
|
||||||
|
[
|
||||||
|
Element(
|
||||||
|
collection_string=root_rest_element.get_collection_string(),
|
||||||
|
id=root_rest_element.pk,
|
||||||
|
full_data=None,
|
||||||
|
reload=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
models.CASCADE(collector, field, sub_objs, using)
|
||||||
|
@ -80,8 +80,6 @@ async def set_config():
|
|||||||
id=config_id,
|
id=config_id,
|
||||||
collection_string=collection_string,
|
collection_string=collection_string,
|
||||||
full_data=full_data,
|
full_data=full_data,
|
||||||
information="",
|
|
||||||
user_id=None,
|
|
||||||
disable_history=True,
|
disable_history=True,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user