From 1d1b7b475b0d4cf89920aa6d4d2836ba40498dba Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 30 May 2025 07:46:47 +0200 Subject: [PATCH 1/4] Added team private map layers --- src/backoffice/views/maps.py | 2 ++ src/maps/migrations/0005_layer_public.py | 20 ++++++++++++ src/maps/mixins.py | 5 +++ src/maps/models.py | 5 +++ src/maps/tests/test_views.py | 40 ++++++++++++++++++++++-- src/maps/views.py | 16 ++++++++-- src/utils/bootstrap/base.py | 13 ++++++++ 7 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/maps/migrations/0005_layer_public.py diff --git a/src/backoffice/views/maps.py b/src/backoffice/views/maps.py index 456ce0d95..221f84181 100644 --- a/src/backoffice/views/maps.py +++ b/src/backoffice/views/maps.py @@ -172,6 +172,7 @@ class MapLayerCreateView(CampViewMixin, AnyTeamMapperRequiredMixin, CreateView): "description", "icon", "invisible", + "public", "group", "responsible_team", ] @@ -208,6 +209,7 @@ class MapLayerUpdateView(CampViewMixin, LayerMapperViewMixin, UpdateView): "description", "icon", "invisible", + "public", "group", "responsible_team", ] diff --git a/src/maps/migrations/0005_layer_public.py b/src/maps/migrations/0005_layer_public.py new file mode 100644 index 000000000..3f7fa9c66 --- /dev/null +++ b/src/maps/migrations/0005_layer_public.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.21 on 2025-05-29 05:03 +from __future__ import annotations + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("maps", "0004_userlocationtype_userlocation"), + ] + + operations = [ + migrations.AddField( + model_name="layer", + name="public", + field=models.BooleanField(default=True, help_text="Make the layer visible to the public. if disabled layer is only visable for team members"), + ), + ] diff --git a/src/maps/mixins.py b/src/maps/mixins.py index f434904f5..bd0e076f0 100644 --- a/src/maps/mixins.py +++ b/src/maps/mixins.py @@ -24,6 +24,11 @@ def setup(self, *args, **kwargs) -> None: """Set self.layer based on layer_slug in url kwargs.""" super().setup(*args, **kwargs) self.layer = get_object_or_404(Layer, slug=self.kwargs["layer_slug"]) + if not self.layer.public: + if (self.layer.responsible_team and self.layer.responsible_team.member_permission_set in self.request.user.get_all_permissions()) or self.request.user.has_perm("camps.gis_team_member"): + return + raise PermissionDenied + def get_context_data(self, *args, **kwargs) -> dict: """Add self.layer to context.""" diff --git a/src/maps/models.py b/src/maps/models.py index 5510f8b0b..210694ca1 100644 --- a/src/maps/models.py +++ b/src/maps/models.py @@ -81,6 +81,11 @@ class Layer(ExportModelOperationsMixin("layer"), UUIDModel): blank=True, ) + public = models.BooleanField( + default=True, + help_text="Make the layer visible to the public. if disabled layer is only visable for team members", + ) + @property def camp(self) -> Camp: """Camp object reference.""" diff --git a/src/maps/tests/test_views.py b/src/maps/tests/test_views.py index 15a15a02a..e99c09666 100644 --- a/src/maps/tests/test_views.py +++ b/src/maps/tests/test_views.py @@ -12,6 +12,7 @@ from django.test.client import RequestFactory from django.urls import reverse +from teams.models import TeamMember from maps.models import Group from maps.models import Layer from maps.models import UserLocation @@ -100,6 +101,7 @@ class MapsViewTest(BornhackTestBase): """Test Maps View""" layer: Layer + hidden_layer: Layer group: Group @classmethod @@ -117,20 +119,54 @@ def setUpTestData(cls) -> None: description="Test Layer", icon="fas fa-tractor", group=cls.group, - responsible_team=cls.teams['noc'], + public=True, + responsible_team=cls.teams["noc"], ) cls.layer.save() + cls.hidden_layer = Layer.objects.create( + name="Non public layer", + slug="hidden_layer", + description="Hidden layer", + icon="fa fa-list-ul", + group=cls.group, + public=False, + responsible_team=cls.teams["noc"], + ) + cls.hidden_layer.save() + + TeamMember.objects.create( + team=cls.teams['noc'], + user=cls.users[0], + approved=True, + lead=True, + ).save() + def test_geojson_layer_views(self) -> None: """Test the geojson view.""" url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": self.layer.slug}) response = self.client.get(url) assert response.status_code == 200 - # Test 404 of geojson layer + # test 404 of geojson layer url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": "123test"}) response = self.client.get(url) assert response.status_code == 404 + + # test layer not being public + url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": self.hidden_layer.slug}) + response = self.client.get(url) + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("p.lead") + matches = [s for s in rows if "403" in str(s)] + self.assertEqual(len(matches), 1, "geojson layer did not return a 403") + + # test layer access when not being public + self.client.force_login(self.users[0]) + url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": self.hidden_layer.slug}) + response = self.client.get(url) + assert response.status_code == 200 def test_map_views(self) -> None: """Test the map view.""" diff --git a/src/maps/views.py b/src/maps/views.py index e4fb3fc6c..522b02f39 100644 --- a/src/maps/views.py +++ b/src/maps/views.py @@ -124,15 +124,25 @@ class MapView(CampViewMixin, TemplateView): template_name = "maps_map.html" context_object_name = "maps_map" + def get_layers(self) -> QuerySet: + user_teams=[] + if self.request.user.id != None: + user_teams = self.request.user.teammember_set.filter( + team__camp=self.camp, + ).values_list("team__name", flat=True) + return Layer.objects.filter( + ((Q(responsible_team__camp=self.camp) | Q(responsible_team=None)) & Q(public=True)) + | (Q(responsible_team__name__in=user_teams) & Q(public=False)), + + ) + def get_context_data(self, **kwargs) -> dict: """Get the context data.""" context = super().get_context_data(**kwargs) context["facilitytype_list"] = FacilityType.objects.filter( responsible_team__camp=self.camp, ) - context["layers"] = Layer.objects.filter( - Q(responsible_team__camp=self.camp) | Q(responsible_team=None), - ) + context["layers"] = self.get_layers() context["user_location_types"] = UserLocationType.objects.filter( user_locations__isnull=False, ).distinct() diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index 2bdd270ee..0dd3756fb 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -1961,6 +1961,7 @@ def create_maps_layer_generic(self) -> None: description="Venue areas", icon="fa fa-list-ul", group=group, + public=True, ) Feature.objects.create( layer=layer, @@ -1989,12 +1990,22 @@ def create_camp_map_layer(self, camp: Camp) -> None: """Create map layers for camp.""" group = MapGroup.objects.get(name="Generic") team = Team.objects.get(name="Orga", camp=camp) + Layer.objects.create( + name="Non public layer", + slug="hiddenlayer", + description="Hidden layer", + icon="fa fa-list-ul", + group=group, + public=False, + team=team, + ) layer = Layer.objects.create( name="Team Area", description="Team areas", icon="fa fa-list-ul", group=group, responsible_team=team, + public=True, ) Feature.objects.create( layer=layer, @@ -2146,6 +2157,8 @@ def bootstrap_tests(self) -> None: year = camp.camp.lower.year teams[year] = self.create_camp_teams(camp) + if year == 2025: + self.add_team_permissions(camp) camp.read_only = read_only camp.call_for_participation_open = not read_only From 45ccf907d4248dc947cf9f5257b960db19d26e09 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 30 May 2025 13:25:13 +0200 Subject: [PATCH 2/4] its ruff --- src/maps/mixins.py | 4 +++- src/maps/tests/test_views.py | 10 +++++----- src/maps/views.py | 3 ++- src/utils/tests.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/maps/mixins.py b/src/maps/mixins.py index bd0e076f0..54cd12b82 100644 --- a/src/maps/mixins.py +++ b/src/maps/mixins.py @@ -25,7 +25,9 @@ def setup(self, *args, **kwargs) -> None: super().setup(*args, **kwargs) self.layer = get_object_or_404(Layer, slug=self.kwargs["layer_slug"]) if not self.layer.public: - if (self.layer.responsible_team and self.layer.responsible_team.member_permission_set in self.request.user.get_all_permissions()) or self.request.user.has_perm("camps.gis_team_member"): + if ((self.layer.responsible_team and + self.layer.responsible_team.member_permission_set in self.request.user.get_all_permissions()) or + self.request.user.has_perm("camps.gis_team_member")): return raise PermissionDenied diff --git a/src/maps/tests/test_views.py b/src/maps/tests/test_views.py index e99c09666..291a2b666 100644 --- a/src/maps/tests/test_views.py +++ b/src/maps/tests/test_views.py @@ -12,13 +12,13 @@ from django.test.client import RequestFactory from django.urls import reverse -from teams.models import TeamMember from maps.models import Group from maps.models import Layer from maps.models import UserLocation from maps.models import UserLocationType from maps.views import MapProxyView from maps.views import MissingCredentialsError +from teams.models import TeamMember from utils.tests import BornhackTestBase USER = "user" @@ -136,7 +136,7 @@ def setUpTestData(cls) -> None: cls.hidden_layer.save() TeamMember.objects.create( - team=cls.teams['noc'], + team=cls.teams["noc"], user=cls.users[0], approved=True, lead=True, @@ -152,8 +152,8 @@ def test_geojson_layer_views(self) -> None: url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": "123test"}) response = self.client.get(url) assert response.status_code == 404 - - # test layer not being public + + # test layer not being public url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": self.hidden_layer.slug}) response = self.client.get(url) content = response.content.decode() @@ -162,7 +162,7 @@ def test_geojson_layer_views(self) -> None: matches = [s for s in rows if "403" in str(s)] self.assertEqual(len(matches), 1, "geojson layer did not return a 403") - # test layer access when not being public + # test layer access when not being public self.client.force_login(self.users[0]) url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": self.hidden_layer.slug}) response = self.client.get(url) diff --git a/src/maps/views.py b/src/maps/views.py index 522b02f39..f0772e815 100644 --- a/src/maps/views.py +++ b/src/maps/views.py @@ -125,8 +125,9 @@ class MapView(CampViewMixin, TemplateView): context_object_name = "maps_map" def get_layers(self) -> QuerySet: + """Method to get the layers the user has access to.""" user_teams=[] - if self.request.user.id != None: + if not self.request.user.is_anonymous: user_teams = self.request.user.teammember_set.filter( team__camp=self.camp, ).values_list("team__name", flat=True) diff --git a/src/utils/tests.py b/src/utils/tests.py index 16d177579..45d9d38a5 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -11,9 +11,9 @@ from camps.models import Camp from teams.models import Team - from utils.bootstrap.base import Bootstrap + class TestBootstrapScript(TestCase): """Test bootstrap_devsite script (touching many codepaths)""" From d6f35da820df59e60166429613179032f00dca7b Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Fri, 30 May 2025 17:10:33 +0200 Subject: [PATCH 3/4] Update src/maps/migrations/0005_layer_public.py --- src/maps/migrations/0005_layer_public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/maps/migrations/0005_layer_public.py b/src/maps/migrations/0005_layer_public.py index 3f7fa9c66..8a47fe0d1 100644 --- a/src/maps/migrations/0005_layer_public.py +++ b/src/maps/migrations/0005_layer_public.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name="layer", name="public", - field=models.BooleanField(default=True, help_text="Make the layer visible to the public. if disabled layer is only visable for team members"), + field=models.BooleanField(default=True, help_text="Make the layer visible to the public. A non-public layer is only visible for team members"), ), ] From 7b4725e29768b1012ecdc91ec5ccc2b1d2132a0b Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Fri, 30 May 2025 17:12:11 +0200 Subject: [PATCH 4/4] Update src/maps/models.py --- src/maps/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/maps/models.py b/src/maps/models.py index 210694ca1..c8f9062fd 100644 --- a/src/maps/models.py +++ b/src/maps/models.py @@ -83,7 +83,7 @@ class Layer(ExportModelOperationsMixin("layer"), UUIDModel): public = models.BooleanField( default=True, - help_text="Make the layer visible to the public. if disabled layer is only visable for team members", + help_text="Make the layer visible to the public. A non-public layer is only visible for team members", ) @property