diff --git a/openslides/core/models.py.orig b/openslides/core/models.py.orig deleted file mode 100644 index a01a18017..000000000 --- a/openslides/core/models.py.orig +++ /dev/null @@ -1,351 +0,0 @@ -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 = () 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/0009_auto_20190119_1425.py b/openslides/users/migrations/0010_auto_20190119_1447.py similarity index 81% rename from openslides/users/migrations/0009_auto_20190119_1425.py rename to openslides/users/migrations/0010_auto_20190119_1447.py index cc096e53b..c44a78480 100644 --- a/openslides/users/migrations/0009_auto_20190119_1425.py +++ b/openslides/users/migrations/0010_auto_20190119_1447.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.5 on 2019-01-19 13:25 +# Generated by Django 2.1.5 on 2019-01-19 13:47 from django.conf import settings from django.db import migrations, models @@ -8,7 +8,7 @@ import openslides.utils.models class Migration(migrations.Migration): - dependencies = [("users", "0008_user_gender")] + dependencies = [("users", "0009_auto_20190119_0941")] operations = [ migrations.AlterField( diff --git a/openslides/users/views.py b/openslides/users/views.py index 13b58d24b..b8f752928 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -569,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: @@ -600,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): @@ -669,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 ab8204ee3..ccdad85c0 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -19,6 +19,18 @@ class ElementBase(TypedDict): 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 diff --git a/openslides/utils/models.py b/openslides/utils/models.py index ac77d9f93..eca3d5013 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -161,11 +161,11 @@ def SET_NULL_AND_AUTOUPDATE( ) -> None: """ Like models.SET_NULL but also informs the autoupdate system about the - instance that was reverenced. + instance that was reference. """ if len(sub_objs) != 1: raise RuntimeError( - "SET_NULL_AND_AUTOUPDATE in an invalid usecase. Please open an issue!" + "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]) @@ -175,11 +175,14 @@ def SET_NULL_AND_AUTOUPDATE( 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 in an invalid usecase. Please open an issue!" + "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( [