diff --git a/openslides/agenda/migrations/0006_auto_20190119_1425.py b/openslides/agenda/migrations/0006_auto_20190119_1425.py new file mode 100644 index 000000000..0166eb741 --- /dev/null +++ b/openslides/agenda/migrations/0006_auto_20190119_1425.py @@ -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, + ), + ), + ] diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 42b655eeb..dac6006e2 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -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. """ diff --git a/openslides/assignments/migrations/0006_auto_20190119_1425.py b/openslides/assignments/migrations/0006_auto_20190119_1425.py new file mode 100644 index 000000000..6840a54fa --- /dev/null +++ b/openslides/assignments/migrations/0006_auto_20190119_1425.py @@ -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, + ), + ), + ] diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index fbcc8f01e..f418a4393 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -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 diff --git a/openslides/core/migrations/0011_auto_20190119_0958.py b/openslides/core/migrations/0011_auto_20190119_0958.py index aa62f1070..c9d5c64dc 100644 --- a/openslides/core/migrations/0011_auto_20190119_0958.py +++ b/openslides/core/migrations/0011_auto_20190119_0958.py @@ -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() + ) ] diff --git a/openslides/core/migrations/0012_auto_20190119_1425.py b/openslides/core/migrations/0012_auto_20190119_1425.py new file mode 100644 index 000000000..3102e48b6 --- /dev/null +++ b/openslides/core/migrations/0012_auto_20190119_1425.py @@ -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", + ), + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 8293f233e..80a1d6117 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -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) diff --git a/openslides/mediafiles/migrations/0003_auto_20190119_1425.py b/openslides/mediafiles/migrations/0003_auto_20190119_1425.py new file mode 100644 index 000000000..66d604bfa --- /dev/null +++ b/openslides/mediafiles/migrations/0003_auto_20190119_1425.py @@ -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, + ), + ) + ] diff --git a/openslides/mediafiles/models.py b/openslides/mediafiles/models.py index 8b5262b37..a602e480c 100644 --- a/openslides/mediafiles/models.py +++ b/openslides/mediafiles/models.py @@ -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.""" diff --git a/openslides/motions/migrations/0019_auto_20190119_1025.py b/openslides/motions/migrations/0019_auto_20190119_1025.py index 9b5f8ec53..b6567505b 100644 --- a/openslides/motions/migrations/0019_auto_20190119_1025.py +++ b/openslides/motions/migrations/0019_auto_20190119_1025.py @@ -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), ), ] diff --git a/openslides/motions/migrations/0020_auto_20190119_1425.py b/openslides/motions/migrations/0020_auto_20190119_1425.py new file mode 100644 index 000000000..cc57844ec --- /dev/null +++ b/openslides/motions/migrations/0020_auto_20190119_1425.py @@ -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", + ), + ), + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 01f915911..29efe2159 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -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.""" diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 3d15ba12f..57892f3eb 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -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."} ) diff --git a/openslides/users/migrations/0009_auto_20190119_0941.py b/openslides/users/migrations/0009_auto_20190119_0941.py index 870297d3e..709b4d8f1 100644 --- a/openslides/users/migrations/0009_auto_20190119_0941.py +++ b/openslides/users/migrations/0009_auto_20190119_0941.py @@ -5,19 +5,23 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('users', '0008_user_gender'), - ] + dependencies = [("users", "0008_user_gender")] operations = [ migrations.AlterModelOptions( - name='user', + name="user", options={ - 'default_permissions': (), 'ordering': ('last_name', 'first_name', 'username'), - 'permissions': ( - ('can_see_name', 'Can see names of users'), - ('can_see_extra_data', 'Can see extra data of users (e.g. present and comment)'), - ('can_change_password', 'Can change its own password'), - ('can_manage', 'Can manage users'))}, - ), + "default_permissions": (), + "ordering": ("last_name", "first_name", "username"), + "permissions": ( + ("can_see_name", "Can see names of users"), + ( + "can_see_extra_data", + "Can see extra data of users (e.g. present and comment)", + ), + ("can_change_password", "Can change its own password"), + ("can_manage", "Can manage users"), + ), + }, + ) ] diff --git a/openslides/users/migrations/0010_auto_20190119_1447.py b/openslides/users/migrations/0010_auto_20190119_1447.py new file mode 100644 index 000000000..c44a78480 --- /dev/null +++ b/openslides/users/migrations/0010_auto_20190119_1447.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.5 on 2019-01-19 13:47 + +from django.conf import settings +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [("users", "0009_auto_20190119_0941")] + + operations = [ + migrations.AlterField( + model_name="personalnote", + name="user", + field=models.OneToOneField( + on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE, + to=settings.AUTH_USER_MODEL, + ), + ) + ] diff --git a/openslides/users/models.py b/openslides/users/models.py index ccaba352a..6cc715b8b 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -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: diff --git a/openslides/users/views.py b/openslides/users/views.py index a12b301b4..b8f752928 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -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, ) ) @@ -571,7 +569,10 @@ class SetPasswordView(APIView): def post(self, request, *args, **kwargs): user = request.user - if not (has_perm(user, "users.can_change_password") or has_perm(user, "users.can_manage")): + if not ( + has_perm(user, "users.can_change_password") + or has_perm(user, "users.can_manage") + ): self.permission_denied(request) if user.check_password(request.data["old_password"]): try: @@ -602,7 +603,10 @@ class PasswordResetView(APIView): """ Loop over all users and send emails. """ - if not (has_perm(request.user, "users.can_change_password") or has_perm(request.user, "users.can_manage")): + if not ( + has_perm(request.user, "users.can_change_password") + or has_perm(request.user, "users.can_manage") + ): self.permission_denied(request) to_email = request.data.get("email") for user in self.get_users(to_email): @@ -671,7 +675,10 @@ class PasswordResetConfirmView(APIView): http_method_names = ["post"] def post(self, request, *args, **kwargs): - if not (has_perm(request.user, "users.can_change_password") or has_perm(request.user, "users.can_manage")): + if not ( + has_perm(request.user, "users.can_change_password") + or has_perm(request.user, "users.can_manage") + ): self.permission_denied(request) uidb64 = request.data.get("user_id") token = request.data.get("token") diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 011301cfc..ccdad85c0 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -9,19 +9,33 @@ 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): + """ + Data container to handle one root rest element for the autoupdate, history + and caching process. + + The fields `id`, `collection_string` and `full_data` are required, the other + fields are optional. + + if full_data is None, it means, that the element was deleted. If reload is + True, full_data is ignored and reloaded from the database later in the + process. + """ + + information: str + user_id: Optional[int] + disable_history: bool + reload: bool + AutoupdateFormat = TypedDict( "AutoupdateFormat", @@ -68,7 +82,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 +113,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 +213,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 +230,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 +243,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. diff --git a/openslides/utils/models.py b/openslides/utils/models.py index b0c931f8b..eca3d5013 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -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,44 @@ 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 reference. + """ + if len(sub_objs) != 1: + raise RuntimeError( + "SET_NULL_AND_AUTOUPDATE is used in an invalid usecase. Please report the bug!" + ) + 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: + """ + Like models.SET_NULL but also informs the autoupdate system about the + root rest element of the also deleted instance. + """ + if len(sub_objs) != 1: + raise RuntimeError( + "CASCADE_AND_AUTOUPDATE is used in an invalid usecase. Please report the bug!" + ) + 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) diff --git a/tests/integration/utils/test_consumers.py b/tests/integration/utils/test_consumers.py index 88bba1b90..da9999dff 100644 --- a/tests/integration/utils/test_consumers.py +++ b/tests/integration/utils/test_consumers.py @@ -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, ) ]