Autoupdate on element deletion

Make sure, that a related element gets an autoupdate, when the main object is deleted
This commit is contained in:
Oskar Hahn 2019-01-19 14:02:13 +01:00
parent a895481cef
commit 11ba7b9841
20 changed files with 737 additions and 70 deletions

View 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,
),
),
]

View File

@ -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.
""" """

View 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,
),
),
]

View File

@ -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

View File

@ -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(),
),
] ]

View 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",
),
),
]

View File

@ -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)

View 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 = ()

View 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,
),
)
]

View File

@ -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."""

View 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),
), ),
] ]

View 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",
),
),
]

View File

@ -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."""

View File

@ -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."}
) )

View 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,
),
)
]

View File

@ -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:

View File

@ -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,
) )
) )

View File

@ -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.

View File

@ -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)

View File

@ -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,
) )
] ]