From 0e6faa067311b20e208f173f4b2ee53d0f2c16d2 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Thu, 26 Dec 2024 19:58:04 +0100 Subject: [PATCH] overlay edit permissions via access restrictions --- src/c3nav/editor/forms.py | 22 +++---- src/c3nav/editor/models/changeset.py | 58 +++++++++++++------ ...130_dataoverlay_edit_access_restriction.py | 19 ++++++ src/c3nav/mapdata/models/overlay.py | 6 +- 4 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0130_dataoverlay_edit_access_restriction.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index d6b22f21..4790b076 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -379,14 +379,14 @@ def create_editor_form(editor_model): 'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', 'todo', 'up_separate', 'bssid', 'main_point', 'external_url', 'external_url_label', 'hub_import_type', 'walk', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', - 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', - "identifyable", 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label', - 'origin_space', 'target_space', 'data', - 'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', "group", - 'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', - "can_report_mistake", 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', - 'enter_description', 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', - 'min_zoom', 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', + 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'edit_access_restriction', 'default_height', + 'door_height', 'outside', 'identifyable', 'can_search', 'can_describe', 'geometry', 'single', 'altitude', + 'level_index', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', + 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', 'group', 'ibeacon_uuid', 'ibeacon_major', + 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', 'can_report_mistake', + 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', 'enter_description', + 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', 'min_zoom', + 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'allow_dynamic_locations', 'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data', 'import_block_geom', 'public', 'default', 'dark', 'high_contrast', 'funky', 'randomize_primary_color', 'color_logo', 'color_css_initial', 'color_css_primary', 'color_css_secondary', 'color_css_tertiary', @@ -396,9 +396,9 @@ def create_editor_form(editor_model): 'color_background', 'color_wall_fill', 'color_wall_border', 'color_door_fill', 'color_ground_fill', 'color_obstacles_default_fill', 'color_obstacles_default_border', 'stroke_color', 'stroke_width', 'stroke_opacity', 'fill_color', 'fill_opacity', 'interactive', 'point_icon', 'extra_data', 'show_label', - 'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', - "load_group_display", "load_group_contribute", - "altitude_quest", + 'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', 'update_interval', + 'load_group_display', 'load_group_contribute', + 'altitude_quest', ] field_names = [field.name for field in editor_model._meta.get_fields() if not field.one_to_many and not isinstance(field, ManyToManyRel)] diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py index 2fed6edd..7b6a489c 100644 --- a/src/c3nav/editor/models/changeset.py +++ b/src/c3nav/editor/models/changeset.py @@ -16,7 +16,8 @@ from django_pydantic_field import SchemaField from c3nav.editor.changes import ChangedObjectCollection, ChangeProblems from c3nav.editor.operations import DatabaseOperationCollection from c3nav.editor.tasks import send_changeset_proposed_notification -from c3nav.mapdata.models import LocationSlug, MapUpdate +from c3nav.mapdata.models import LocationSlug, MapUpdate, DataOverlayFeature, DataOverlay +from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.models.locations import LocationRedirect @@ -59,12 +60,6 @@ class ChangeSet(models.Model): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.created_objects = {} - self.updated_existing = {} - self.deleted_existing = {} - self.m2m_added = {} - self.m2m_removed = {} - self._object_changed = False self._request = None self._original_state = self.state @@ -179,40 +174,67 @@ class ChangeSet(models.Model): return (self.can_edit(request) and self.can_review(request) and not self.proposed and self.changes and not self.problems.any) - def has_space_access_on_all_objects(self, request, force=False): + def has_edit_access_on_all_objects(self, request, force=False): # todo: reimplement this if not request.user.is_authenticated: return False try: - request._has_space_access_on_all_objects_cache + request._has_edit_access_on_all_objects_cache except AttributeError: - request._has_space_access_on_all_objects_cache = {} + request._has_edit_access_on_all_objects_cache = {} can_edit_spaces = {space_id for space_id, can_edit in request.user_space_accesses.items() if can_edit} - if not can_edit_spaces: - return False - if not force: try: - return request._has_space_access_on_all_objects_cache[self.pk] + return request._has_edit_access_on_all_objects_cache[self.pk] except KeyError: pass - for model in self.changed_objects.keys(): + for modelname in self.changes.objects.keys(): + model = apps.get_model('mapdata', modelname) if issubclass(model, LocationRedirect): continue + if issubclass(model, (DataOverlay, DataOverlayFeature)): + continue try: model._meta.get_field('space') except FieldDoesNotExist: return False + permissions = AccessPermission.get_for_request(request) + + overlay_ids = [] + + result = True - for model, objects in self.get_objects(many=False).items(): + for model_name, objects in self.changes.objects.items(): + model = apps.get_model('mapdata', model_name) if issubclass(model, (LocationRedirect, LocationSlug)): continue + if issubclass(model, DataOverlay): + ids = [obj.obj.id for obj in objects.values()] + restriction_ids = set(model.objects.filter(pk__in=ids).values_list('edit_access_restriction_id', flat=True)) + if None in restriction_ids or (restriction_ids - permissions - {None}): + result = False + break + + continue + + if issubclass(model, DataOverlayFeature): + ids = [obj.obj.id for obj in objects.values()] + + restriction_ids = set(model.objects + .filter(pk__in=ids) + .values_list('overlay__edit_access_restriction_id', flat=True)) + if None in restriction_ids or (restriction_ids - permissions - {None}): + result = False + break + + continue + try: model._meta.get_field('space') except FieldDoesNotExist: @@ -250,7 +272,7 @@ class ChangeSet(models.Model): if not result: break - request._has_space_access_on_all_objects_cache[self.pk] = result + request._has_edit_access_on_all_objects_cache[self.pk] = result return result def can_review(self, request): @@ -258,7 +280,7 @@ class ChangeSet(models.Model): return False if request.user_permissions.review_changesets: return True - return self.has_space_access_on_all_objects(request) + return self.has_edit_access_on_all_objects(request) @classmethod def can_direct_edit(cls, request): diff --git a/src/c3nav/mapdata/migrations/0130_dataoverlay_edit_access_restriction.py b/src/c3nav/mapdata/migrations/0130_dataoverlay_edit_access_restriction.py new file mode 100644 index 00000000..c17a67b8 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0130_dataoverlay_edit_access_restriction.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.3 on 2024-12-26 18:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0129_dataoverlay_cluster_points'), + ] + + operations = [ + migrations.AddField( + model_name='dataoverlay', + name='edit_access_restriction', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='edit_access_restrictions', to='mapdata.accessrestriction', verbose_name='Editor Access Restriction'), + ), + ] diff --git a/src/c3nav/mapdata/models/overlay.py b/src/c3nav/mapdata/models/overlay.py index e6b47c53..89b170cd 100644 --- a/src/c3nav/mapdata/models/overlay.py +++ b/src/c3nav/mapdata/models/overlay.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ from django_pydantic_field import SchemaField from c3nav.mapdata.fields import GeometryField -from c3nav.mapdata.models.access import AccessRestrictionMixin +from c3nav.mapdata.models.access import AccessRestrictionMixin, AccessRestriction from c3nav.mapdata.models.base import TitledMixin from c3nav.mapdata.models.geometry.level import LevelGeometryMixin from c3nav.mapdata.utils.geometry import smart_mapping @@ -34,6 +34,10 @@ class DataOverlay(TitledMixin, AccessRestrictionMixin, models.Model): pull_headers: dict[str, str] = SchemaField(schema=dict[str, str], null=True, verbose_name=_('headers for pull http request (JSON object)')) pull_interval = models.DurationField(blank=True, null=True, verbose_name=_('pull interval')) + edit_access_restriction = models.ForeignKey(AccessRestriction, null=True, blank=True, + related_name='edit_access_restrictions', + verbose_name=_('Editor Access Restriction'), + on_delete=models.PROTECT) class Meta: verbose_name = _('Data Overlay')