diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7cddde4a2..a29a6ec4f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,11 +17,12 @@ Core: - Enabled docs for using OpenSlides with Gunicorn and Uvicorn in big mode [#3799, #3817]. - Changed format for elements send via autoupdate [#3926]. + - Fixed autoupdate system for related objects [#4140]. - Add a change-id system to get only new elements [#3938]. - Switch from Yarn back to npm [#3964]. - Added password reset link (password reset via email) [#3914]. - - Added global history mode [#3977]. - - Projector Refactor [4119, #4130]. + - Added global history mode [#3977, #4141]. + - Projector refactoring [4119, #4130]. Agenda: - Added viewpoint to assign multiple items to a new parent item [#4037]. diff --git a/openslides/core/migrations/0013_auto_20190119_1641.py b/openslides/core/migrations/0013_auto_20190119_1641.py new file mode 100644 index 000000000..108409989 --- /dev/null +++ b/openslides/core/migrations/0013_auto_20190119_1641.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.5 on 2019-01-19 15:41 + +import jsonfield.encoder +import jsonfield.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_auto_20190119_1425'), + ] + + operations = [ + migrations.AddField( + model_name='history', + name='restricted', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='history', + name='information', + field=jsonfield.fields.JSONField(dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}), + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 80a1d6117..a9eeee8d8 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -286,7 +286,8 @@ class HistoryManager(models.Manager): element["collection_string"], element["id"] ), now=history_time, - information=element.get("information", ""), + information=element.get("information", []), + restricted=element.get("restricted", False), user_id=element.get("user_id"), full_data=data, ) @@ -334,7 +335,9 @@ class History(RESTModelMixin, models.Model): now = models.DateTimeField() - information = models.CharField(max_length=255) + information = JSONField() + + restricted = models.BooleanField(default=False) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, on_delete=SET_NULL_AND_AUTOUPDATE diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 2f5faa379..c95e69cc8 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -169,5 +169,5 @@ class HistorySerializer(ModelSerializer): class Meta: model = History - fields = ("id", "element_id", "now", "information", "user") + fields = ("id", "element_id", "now", "information", "restricted", "user") read_only_fields = ("now",) diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 57892f3eb..289427317 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -124,7 +124,7 @@ class MotionViewSet(ModelViewSet): # Fire autoupdate again to save information to OpenSlides history. inform_deleted_data( [(motion.get_collection_string(), motion.pk)], - information="Motion deleted", + information=["Motion deleted"], user_id=request.user.pk, ) @@ -221,10 +221,16 @@ class MotionViewSet(ModelViewSet): # Send new submitters and supporters via autoupdate because users # without permission to see users may not have them but can get it now. + # TODO: Skip history. new_users = list(motion.submitters.all()) new_users.extend(motion.supporters.all()) inform_changed_data(new_users) + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=["Motion created"], user_id=request.user.pk, + ) + headers = self.get_success_headers(serializer.data) # Strip out response data so nobody gets unrestricted data. data = ReturnDict(id=serializer.data.get("id"), serializer=serializer) @@ -298,12 +304,13 @@ class MotionViewSet(ModelViewSet): # Send new supporters via autoupdate because users # without permission to see users may not have them but can get it now. + # TODO: Skip history. new_users = list(updated_motion.supporters.all()) inform_changed_data(new_users) # Fire autoupdate again to save information to OpenSlides history. inform_changed_data( - updated_motion, information="Motion updated", user_id=request.user.pk + updated_motion, information=["Motion updated"], user_id=request.user.pk ) # We do not add serializer.data to response so nobody gets unrestricted data here. @@ -396,7 +403,7 @@ class MotionViewSet(ModelViewSet): # write log motion.write_log([f"Comment {section.name} updated"], request.user) - message = f"Comment {section.name} updated" + message = ["Comment {arg1} updated", section.name] else: # DELETE try: comment = MotionComment.objects.get(motion=motion, section=section) @@ -407,7 +414,12 @@ class MotionViewSet(ModelViewSet): comment.delete() motion.write_log([f"Comment {section.name} deleted"], request.user) - message = f"Comment {section.name} deleted" + message = ["Comment {arg1} deleted", section.name] + + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=message, user_id=request.user.pk, restricted=True, + ) return Response({"detail": message}) @@ -475,7 +487,7 @@ class MotionViewSet(ModelViewSet): motion_result.append(motion) # Now inform all clients. - inform_changed_data(motion_result) + inform_changed_data(motion_result, information=["Submitters changed"], user_id=request.user.pk) # Also send all new submitters via autoupdate because users without # permission to see users may not have them but can get it now. @@ -512,6 +524,7 @@ class MotionViewSet(ModelViewSet): motion.write_log(["Motion supported"], request.user) # Send new supporter via autoupdate because users without permission # to see users may not have it but can get it now. + # TODO: Skip history. inform_changed_data([request.user]) message = "You have supported this motion successfully." else: @@ -523,6 +536,11 @@ class MotionViewSet(ModelViewSet): motion.write_log(["Motion unsupported"], request.user) message = "You have unsupported this motion successfully." + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=["Supporters changed"], user_id=request.user.pk + ) + # Initiate response. return Response({"detail": message}) @@ -569,11 +587,10 @@ 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, - ) + + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data(motion, information=["State set to {arg1}", motion.state.name], user_id=request.user.pk) + return Response({"detail": message}) @list_route(methods=["post"]) @@ -640,15 +657,17 @@ class MotionViewSet(ModelViewSet): skip_autoupdate=True, ) + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=["State set to {arg1}", motion.state.name], user_id=request.user.pk, + ) + # Finish motion. motion_result.append(motion) - # Now inform all clients. - inform_changed_data(motion_result) - # Send response. return Response( - {"detail": f"{len(motion_result)} motions successfully updated."} + {"detail": f"State of {len(motion_result)} motions successfully set."} ) @detail_route(methods=["put"]) @@ -703,7 +722,12 @@ class MotionViewSet(ModelViewSet): person=request.user, skip_autoupdate=True, ) - inform_changed_data(motion) + + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=["Recommendation set to {arg1}", label], user_id=request.user.pk, + ) + return Response({"detail": message}) @list_route(methods=["post"]) @@ -784,12 +808,14 @@ class MotionViewSet(ModelViewSet): skip_autoupdate=True, ) + # Fire autoupdate and save information to OpenSlides history. + inform_changed_data( + motion, information=["Recommendation set to {arg1}", label], user_id=request.user.pk, + ) + # Finish motion. motion_result.append(motion) - # Now inform all clients. - inform_changed_data(motion_result) - # Send response. return Response( {"detail": f"{len(motion_result)} motions successfully updated."} @@ -826,7 +852,8 @@ class MotionViewSet(ModelViewSet): ) # Now send all changes to the clients. - inform_changed_data(motion) + inform_changed_data(motion, information=["State set to {arg1}", motion.state.name], user_id=request.user.pk) + return Response({"detail": "Recommendation followed successfully."}) @detail_route(methods=["post"]) @@ -846,7 +873,11 @@ class MotionViewSet(ModelViewSet): raise ValidationError({"detail": err}) motion.write_log(["Vote created"], request.user, skip_autoupdate=True) - inform_changed_data(motion) + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + motion, information=["Vote created"], user_id=request.user.pk, + ) + return Response( {"detail": "Vote created successfully.", "createdPollId": poll.pk} ) @@ -939,6 +970,12 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): response = super().update(*args, **kwargs) poll = self.get_object() poll.motion.write_log(["Vote updated"], self.request.user) + + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + poll.motion, information=["Poll updated"], user_id=self.request.user.pk + ) + return response def destroy(self, *args, **kwargs): @@ -948,6 +985,12 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): poll = self.get_object() result = super().destroy(*args, **kwargs) poll.motion.write_log(["Vote deleted"], self.request.user) + + # Fire autoupdate again to save information to OpenSlides history. + inform_changed_data( + poll.motion, information=["Poll deleted"], user_id=self.request.user.pk + ) + return result @@ -1205,7 +1248,7 @@ class CategoryViewSet(ModelViewSet): error_message = "Error: At least one identifier of this category does already exist in another category." response = Response({"detail": error_message}, status=400) else: - inform_changed_data(instances) + inform_changed_data(instances, information=["Number set"], user_id=request.user.pk) message = f"All motions in category {category} numbered " "successfully." response = Response({"detail": message}) return response @@ -1251,7 +1294,6 @@ class MotionBlockViewSet(ModelViewSet): its recommendation. It is a POST request without any data. """ motion_block = self.get_object() - instances = [] with transaction.atomic(): for motion in motion_block.motion_set.all(): # Follow recommendation. @@ -1263,8 +1305,10 @@ class MotionBlockViewSet(ModelViewSet): person=request.user, skip_autoupdate=True, ) - instances.append(motion) - inform_changed_data(instances) + # Fire autoupdate and save information to OpenSlides history. + inform_changed_data( + motion, information=["State set to {arg1}", motion.state.name], user_id=request.user.pk + ) return Response({"detail": "Followed recommendations successfully."}) diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index ccdad85c0..f5d8dbad8 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -31,7 +31,8 @@ class Element(ElementBase, total=False): process. """ - information: str + information: List[str] + restricted: bool user_id: Optional[int] disable_history: bool reload: bool @@ -51,8 +52,9 @@ AutoupdateFormat = TypedDict( def inform_changed_data( instances: Union[Iterable[Model], Model], - information: str = "", + information: List[str] = None, user_id: Optional[int] = None, + restricted: bool = False, ) -> None: """ Informs the autoupdate system and the caching system about the creation or @@ -62,6 +64,8 @@ def inform_changed_data( History creation is enabled. """ + if information is None: + information = [] root_instances = set() if not isinstance(instances, Iterable): instances = (instances,) @@ -81,6 +85,7 @@ def inform_changed_data( collection_string=root_instance.get_collection_string(), full_data=root_instance.get_full_data(), information=information, + restricted=restricted, user_id=user_id, ) @@ -95,8 +100,9 @@ def inform_changed_data( def inform_deleted_data( deleted_elements: Iterable[Tuple[str, int]], - information: str = "", + information: List[str] = None, user_id: Optional[int] = None, + restricted: bool = False, ) -> None: """ Informs the autoupdate system and the caching system about the deletion of @@ -104,6 +110,8 @@ def inform_deleted_data( History creation is enabled. """ + if information is None: + information = [] elements: Dict[str, Element] = {} for deleted_element in deleted_elements: key = deleted_element[0] + str(deleted_element[1]) @@ -112,6 +120,7 @@ def inform_deleted_data( collection_string=deleted_element[0], full_data=None, information=information, + restricted=restricted, user_id=user_id, )