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.utils import to_roman
|
||||
|
||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||
from .access_permissions import ItemAccessPermissions
|
||||
|
||||
|
||||
@ -233,7 +234,7 @@ class Item(RESTModelMixin, models.Model):
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
@ -374,7 +375,7 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
|
||||
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.
|
||||
"""
|
||||
|
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.models import RESTModelMixin
|
||||
|
||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||
from .access_permissions import AssignmentAccessPermissions
|
||||
|
||||
|
||||
@ -36,7 +37,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||
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.
|
||||
"""
|
||||
@ -366,7 +367,9 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
poll = models.ForeignKey(
|
||||
"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)
|
||||
|
||||
vote_class = AssignmentVote
|
||||
|
@ -5,14 +5,10 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_auto_20190118_1908'),
|
||||
]
|
||||
dependencies = [("core", "0010_auto_20190118_1908")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='history',
|
||||
name='now',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
model_name="history", 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.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 (
|
||||
ChatMessageAccessPermissions,
|
||||
ConfigAccessPermissions,
|
||||
@ -108,7 +112,7 @@ class ProjectionDefault(RESTModelMixin, models.Model):
|
||||
display_name = models.CharField(max_length=256)
|
||||
|
||||
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):
|
||||
@ -179,7 +183,7 @@ class ChatMessage(RESTModelMixin, models.Model):
|
||||
|
||||
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:
|
||||
default_permissions = ()
|
||||
@ -269,7 +273,7 @@ class HistoryManager(models.Manager):
|
||||
history_time = now()
|
||||
for element in elements:
|
||||
if (
|
||||
element["disable_history"]
|
||||
element.get("disable_history")
|
||||
or element["collection_string"]
|
||||
== self.model.get_collection_string()
|
||||
):
|
||||
@ -282,8 +286,8 @@ class HistoryManager(models.Manager):
|
||||
element["collection_string"], element["id"]
|
||||
),
|
||||
now=history_time,
|
||||
information=element["information"],
|
||||
user_id=element["user_id"],
|
||||
information=element.get("information", ""),
|
||||
user_id=element.get("user_id"),
|
||||
full_data=data,
|
||||
)
|
||||
instance.save(
|
||||
@ -308,9 +312,6 @@ class HistoryManager(models.Manager):
|
||||
id=full_data["id"],
|
||||
collection_string=collection_string,
|
||||
full_data=full_data,
|
||||
information="",
|
||||
user_id=None,
|
||||
disable_history=False,
|
||||
)
|
||||
)
|
||||
instances = self.add_elements(elements)
|
||||
@ -336,7 +337,7 @@ class History(RESTModelMixin, models.Model):
|
||||
information = models.CharField(max_length=255)
|
||||
|
||||
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)
|
||||
|
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 ..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
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
"""A string representing the title of the file."""
|
||||
|
||||
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."""
|
||||
|
||||
|
@ -6,20 +6,20 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0018_auto_20190118_2101'),
|
||||
]
|
||||
dependencies = [("motions", "0018_auto_20190118_2101")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
model_name="motion",
|
||||
name="created",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='last_modified',
|
||||
model_name="motion",
|
||||
name="last_modified",
|
||||
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.models import RESTModelMixin
|
||||
|
||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
@ -141,7 +142,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
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.
|
||||
@ -171,7 +172,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
sort_parent = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
@ -181,14 +182,14 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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.
|
||||
@ -207,7 +208,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="amendments",
|
||||
@ -220,7 +221,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
statute_paragraph = models.ForeignKey(
|
||||
StatuteParagraph,
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="motions",
|
||||
@ -704,7 +705,7 @@ class Submitter(RESTModelMixin, models.Model):
|
||||
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.
|
||||
"""
|
||||
@ -754,7 +755,7 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
objects = MotionChangeRecommendationManager()
|
||||
|
||||
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."""
|
||||
|
||||
@ -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"""
|
||||
|
||||
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."""
|
||||
|
||||
@ -921,7 +922,7 @@ class MotionLog(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
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."""
|
||||
|
||||
@ -1192,7 +1193,7 @@ class Workflow(RESTModelMixin, models.Model):
|
||||
"""A string representing the workflow."""
|
||||
|
||||
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."""
|
||||
|
||||
|
@ -569,7 +569,11 @@ class MotionViewSet(ModelViewSet):
|
||||
person=request.user,
|
||||
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})
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@ -1348,9 +1352,8 @@ class StateViewSet(
|
||||
Customized view endpoint to delete a state.
|
||||
"""
|
||||
state = self.get_object()
|
||||
if (
|
||||
state.workflow.first_state.pk == state.pk
|
||||
): # is this the first state of the workflow?
|
||||
if state.workflow.first_state.pk == state.pk:
|
||||
# is this the first state of the workflow?
|
||||
raise ValidationError(
|
||||
{"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 ..utils.auth import GROUP_ADMIN_PK
|
||||
from ..utils.models import RESTModelMixin
|
||||
from ..utils.models import CASCADE_AND_AUTOUODATE, RESTModelMixin
|
||||
from .access_permissions import (
|
||||
GroupAccessPermissions,
|
||||
PersonalNoteAccessPermissions,
|
||||
@ -330,7 +330,7 @@ class PersonalNote(RESTModelMixin, models.Model):
|
||||
|
||||
objects = PersonalNoteManager()
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUODATE)
|
||||
notes = JSONField()
|
||||
|
||||
class Meta:
|
||||
|
@ -369,8 +369,6 @@ class GroupViewSet(ModelViewSet):
|
||||
id=full_data["id"],
|
||||
collection_string=cachable.get_collection_string(),
|
||||
full_data=full_data,
|
||||
information="",
|
||||
user_id=None,
|
||||
disable_history=True,
|
||||
)
|
||||
)
|
||||
|
@ -9,19 +9,21 @@ from mypy_extensions import TypedDict
|
||||
|
||||
from .cache import element_cache, get_element_id
|
||||
from .projector import get_projectot_data
|
||||
from .utils import get_model_from_collection_string
|
||||
|
||||
|
||||
Element = TypedDict(
|
||||
"Element",
|
||||
{
|
||||
"id": int,
|
||||
"collection_string": str,
|
||||
"full_data": Optional[Dict[str, Any]],
|
||||
"information": str,
|
||||
"user_id": Optional[int],
|
||||
"disable_history": bool,
|
||||
},
|
||||
)
|
||||
class ElementBase(TypedDict):
|
||||
id: int
|
||||
collection_string: str
|
||||
full_data: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class Element(ElementBase, total=False):
|
||||
information: str
|
||||
user_id: Optional[int]
|
||||
disable_history: bool
|
||||
reload: bool
|
||||
|
||||
|
||||
AutoupdateFormat = TypedDict(
|
||||
"AutoupdateFormat",
|
||||
@ -68,7 +70,6 @@ def inform_changed_data(
|
||||
full_data=root_instance.get_full_data(),
|
||||
information=information,
|
||||
user_id=user_id,
|
||||
disable_history=False,
|
||||
)
|
||||
|
||||
bundle = autoupdate_bundle.get(threading.get_ident())
|
||||
@ -100,7 +101,6 @@ def inform_deleted_data(
|
||||
full_data=None,
|
||||
information=information,
|
||||
user_id=user_id,
|
||||
disable_history=False,
|
||||
)
|
||||
|
||||
bundle = autoupdate_bundle.get(threading.get_ident())
|
||||
@ -201,6 +201,12 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
||||
)
|
||||
|
||||
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.
|
||||
history_instances = save_history(elements)
|
||||
|
||||
@ -212,8 +218,6 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
||||
id=history_instance.get_rest_pk(),
|
||||
collection_string=history_instance.get_collection_string(),
|
||||
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.
|
||||
)
|
||||
)
|
||||
@ -227,9 +231,8 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def save_history(
|
||||
elements: Iterable[Element]
|
||||
) -> Iterable: # TODO: Try to write Iterable[History] here
|
||||
def save_history(elements: Iterable[Element]) -> Iterable:
|
||||
# TODO: Try to write Iterable[History] here
|
||||
"""
|
||||
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 .access_permissions import BaseAccessPermissions
|
||||
from .autoupdate import Element, inform_changed_data, inform_changed_elements
|
||||
from .rest_api import model_serializer_classes
|
||||
from .utils import convert_camel_case_to_pseudo_snake_case
|
||||
|
||||
@ -153,3 +154,41 @@ class RESTModelMixin:
|
||||
__import__(module_name)
|
||||
serializer_class = model_serializer_classes[type(self)]
|
||||
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,
|
||||
collection_string=collection_string,
|
||||
full_data=full_data,
|
||||
information="",
|
||||
user_id=None,
|
||||
disable_history=True,
|
||||
)
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user