overlay edit permissions via access restrictions

This commit is contained in:
Gwendolyn 2024-12-26 19:58:04 +01:00
parent e1b820ae89
commit 0e6faa0673
4 changed files with 75 additions and 30 deletions

View file

@ -379,14 +379,14 @@ def create_editor_form(editor_model):
'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', 'todo', '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', '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', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name',
'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'edit_access_restriction', 'default_height',
"identifyable", 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label', 'door_height', 'outside', 'identifyable', 'can_search', 'can_describe', 'geometry', 'single', 'altitude',
'origin_space', 'target_space', 'data', 'level_index', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor',
'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', "group", 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', 'group', 'ibeacon_uuid', 'ibeacon_major',
'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', 'can_report_mistake',
"can_report_mistake", 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', 'enter_description',
'enter_description', 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', 'min_zoom',
'min_zoom', 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', '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', 'allow_dynamic_locations', 'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data',
'import_block_geom', 'public', 'default', 'dark', 'high_contrast', 'funky', 'randomize_primary_color', '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', '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_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', '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', 'stroke_opacity', 'fill_color', 'fill_opacity', 'interactive', 'point_icon', 'extra_data', 'show_label',
'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', 'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', 'update_interval',
"load_group_display", "load_group_contribute", 'load_group_display', 'load_group_contribute',
"altitude_quest", 'altitude_quest',
] ]
field_names = [field.name for field in editor_model._meta.get_fields() field_names = [field.name for field in editor_model._meta.get_fields()
if not field.one_to_many and not isinstance(field, ManyToManyRel)] if not field.one_to_many and not isinstance(field, ManyToManyRel)]

View file

@ -16,7 +16,8 @@ from django_pydantic_field import SchemaField
from c3nav.editor.changes import ChangedObjectCollection, ChangeProblems from c3nav.editor.changes import ChangedObjectCollection, ChangeProblems
from c3nav.editor.operations import DatabaseOperationCollection from c3nav.editor.operations import DatabaseOperationCollection
from c3nav.editor.tasks import send_changeset_proposed_notification 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 from c3nav.mapdata.models.locations import LocationRedirect
@ -59,12 +60,6 @@ class ChangeSet(models.Model):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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._object_changed = False
self._request = None self._request = None
self._original_state = self.state self._original_state = self.state
@ -179,40 +174,67 @@ class ChangeSet(models.Model):
return (self.can_edit(request) and self.can_review(request) return (self.can_edit(request) and self.can_review(request)
and not self.proposed and self.changes and not self.problems.any) 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 # todo: reimplement this
if not request.user.is_authenticated: if not request.user.is_authenticated:
return False return False
try: try:
request._has_space_access_on_all_objects_cache request._has_edit_access_on_all_objects_cache
except AttributeError: 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} 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: if not force:
try: 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: except KeyError:
pass 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): if issubclass(model, LocationRedirect):
continue continue
if issubclass(model, (DataOverlay, DataOverlayFeature)):
continue
try: try:
model._meta.get_field('space') model._meta.get_field('space')
except FieldDoesNotExist: except FieldDoesNotExist:
return False return False
permissions = AccessPermission.get_for_request(request)
overlay_ids = []
result = True 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)): if issubclass(model, (LocationRedirect, LocationSlug)):
continue 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: try:
model._meta.get_field('space') model._meta.get_field('space')
except FieldDoesNotExist: except FieldDoesNotExist:
@ -250,7 +272,7 @@ class ChangeSet(models.Model):
if not result: if not result:
break 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 return result
def can_review(self, request): def can_review(self, request):
@ -258,7 +280,7 @@ class ChangeSet(models.Model):
return False return False
if request.user_permissions.review_changesets: if request.user_permissions.review_changesets:
return True return True
return self.has_space_access_on_all_objects(request) return self.has_edit_access_on_all_objects(request)
@classmethod @classmethod
def can_direct_edit(cls, request): def can_direct_edit(cls, request):

View file

@ -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'),
),
]

View file

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from django_pydantic_field import SchemaField from django_pydantic_field import SchemaField
from c3nav.mapdata.fields import GeometryField 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.base import TitledMixin
from c3nav.mapdata.models.geometry.level import LevelGeometryMixin from c3nav.mapdata.models.geometry.level import LevelGeometryMixin
from c3nav.mapdata.utils.geometry import smart_mapping 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, pull_headers: dict[str, str] = SchemaField(schema=dict[str, str], null=True,
verbose_name=_('headers for pull http request (JSON object)')) verbose_name=_('headers for pull http request (JSON object)'))
pull_interval = models.DurationField(blank=True, null=True, verbose_name=_('pull interval')) 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: class Meta:
verbose_name = _('Data Overlay') verbose_name = _('Data Overlay')