overlay edit permissions via access restrictions
This commit is contained in:
parent
e1b820ae89
commit
0e6faa0673
4 changed files with 75 additions and 30 deletions
|
@ -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)]
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue