diff --git a/pyproject.toml b/pyproject.toml index d117f719f..f3151b79d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ test = [ "coverage==7.8.0", "factory_boy==3.3.3", "hypothesis==6.131.15", + "beautifulsoup4==4.13.4", "unittest-xml-reporting==3.2.0", ] @@ -123,7 +124,8 @@ ignore = [ "ANN001", # Missing type annotation for function argument ... "ANN201", # Missing return type annotation for public function ... "S101", # Allow use of assert - "S106", # Allow passwords in tests + "S105", # Allow PASSWORD as password in tests + "S106", # Allow passwords in tests "S113", # Probable use of requests call without timeout "E501", # Line too long "D104", # Missing docstring in public package diff --git a/src/bornhack/environment_settings.py.dist b/src/bornhack/environment_settings.py.dist index 21c80f1d1..8afd8462f 100644 --- a/src/bornhack/environment_settings.py.dist +++ b/src/bornhack/environment_settings.py.dist @@ -76,6 +76,9 @@ SCHEDULE_TIMESLOT_LENGTH_MINUTES=30 SCHEDULE_EVENT_NOTIFICATION_MINUTES=10 SPEAKER_AVAILABILITY_DAYCHUNK_HOURS=3 # how many hours per speaker_availability form checkbox +# Map settings +MAPS_USER_LOCATION_MAX = 50 # Maximum number of UserLocations a user can create + # irc bot settings IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10 IRCBOT_NICK='{{ django_ircbot_nickname }}' diff --git a/src/bornhack/environment_settings.py.dist.dev b/src/bornhack/environment_settings.py.dist.dev index a4ae54373..55097a7ce 100644 --- a/src/bornhack/environment_settings.py.dist.dev +++ b/src/bornhack/environment_settings.py.dist.dev @@ -39,6 +39,9 @@ SCHEDULE_TIMESLOT_LENGTH_MINUTES = 30 SCHEDULE_EVENT_NOTIFICATION_MINUTES = 10 SPEAKER_AVAILABILITY_DAYCHUNK_HOURS=3 +# Map settings +MAPS_USER_LOCATION_MAX = 50 # Maximum number of UserLocations a user can create + PDF_TEST_MODE = True PDF_ARCHIVE_PATH = os.path.join(MEDIA_ROOT, "pdf_archive") diff --git a/src/maps/__init__.py b/src/maps/__init__.py index e69de29bb..3bfb95a8c 100644 --- a/src/maps/__init__.py +++ b/src/maps/__init__.py @@ -0,0 +1 @@ +"""Maps Application.""" diff --git a/src/maps/admin.py b/src/maps/admin.py index 0a7e0effe..8d81fe024 100644 --- a/src/maps/admin.py +++ b/src/maps/admin.py @@ -1,5 +1,9 @@ +"""Maps Django Admin.""" + from __future__ import annotations +from typing import ClassVar + from django.contrib import admin from leaflet.admin import LeafletGeoAdmin @@ -13,47 +17,56 @@ @admin.register(Feature) class FeatureAdmin(LeafletGeoAdmin, admin.ModelAdmin): + """Feature Admin.""" + display_raw = True save_as = True - list_display = [ + list_display: ClassVar[list[str]] = [ "name", "description", ] - list_filter = [ + list_filter: ClassVar[list[str]] = [ "layer", ] - def get_queryset(self, request): - self.request = request - return super().get_queryset(request) @admin.register(Layer) class LayerAdmin(admin.ModelAdmin): + """Layer admin.""" + save_as = True - list_display = ["name", "slug"] + list_display: ClassVar[list[str]] = ["name", "slug"] @admin.register(ExternalLayer) class ExternalLayerAdmin(admin.ModelAdmin): + """Layer admin.""" + save_as = True - list_display = ["name"] + list_display: ClassVar[list[str]] = ["name"] @admin.register(Group) class GroupAdmin(admin.ModelAdmin): + """Group admin.""" + save_as = True - list_display = ["name"] + list_display: ClassVar[list[str]] = ["name"] @admin.register(UserLocationType) class UserLocationTypeAdmin(admin.ModelAdmin): + """User Location Type admin.""" + save_as = True - list_display = ["name"] + list_display: ClassVar[list[str]] = ["name"] @admin.register(UserLocation) class UserLocationAdmin(admin.ModelAdmin): + """User Location admin.""" + save_as = True - list_display = ["name", "type", "user", "camp"] - list_filter = ["camp", "user"] + list_display: ClassVar[list[str]] = ["name", "type", "user", "camp"] + list_filter: ClassVar[list[str]] = ["camp", "user"] diff --git a/src/maps/apps.py b/src/maps/apps.py index 4701d6a44..6dfda87f1 100644 --- a/src/maps/apps.py +++ b/src/maps/apps.py @@ -1,7 +1,11 @@ +"""Apps for the Maps app.""" + from __future__ import annotations from django.apps import AppConfig class MapsConfig(AppConfig): + """Maps config.""" + name = "maps" diff --git a/src/maps/mixins.py b/src/maps/mixins.py index 5b1839eac..f434904f5 100644 --- a/src/maps/mixins.py +++ b/src/maps/mixins.py @@ -1,5 +1,12 @@ +"""Mixins for Maps app.""" + from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from django.http import HttpRequest + from django.contrib import messages from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 @@ -14,19 +21,26 @@ class LayerViewMixin: """A mixin to get the Layer object based on layer_slug in url kwargs.""" 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"]) - def get_context_data(self, *args, **kwargs): + def get_context_data(self, *args, **kwargs) -> dict: + """Add self.layer to context.""" context = super().get_context_data(*args, **kwargs) context["layer"] = self.layer return context class LayerMapperViewMixin(LayerViewMixin): - """A mixin for views only available to users with mapper permission for the team responsible for the layer and/or Mapper team permission.""" + """A mixin for LayerMapper. - def setup(self, request, *args, **kwargs) -> None: + For views only available to users with mapper permission for the team responsible + for the layer and/or Mapper team permission. + """ + + def setup(self, request: HttpRequest, *args, **kwargs) -> None: + """Check permissions.""" super().setup(request, *args, **kwargs) if ( self.layer.responsible_team @@ -40,7 +54,8 @@ def setup(self, request, *args, **kwargs) -> None: class GisTeamViewMixin: """A mixin for views only available to users with `camps.gis_team_member` permission.""" - def setup(self, request, *args, **kwargs) -> None: + def setup(self, request: HttpRequest, *args, **kwargs) -> None: + """Check permissions.""" super().setup(request, *args, **kwargs) if self.request.user.has_perm("camps.gis_team_member"): return @@ -52,6 +67,7 @@ class ExternalLayerViewMixin(CampViewMixin): """A mixin to get the ExternalLayer object based on external_layer_uuid in url kwargs.""" def setup(self, *args, **kwargs) -> None: + """Set self.layer.""" super().setup(*args, **kwargs) self.layer = get_object_or_404( ExternalLayer, @@ -60,9 +76,14 @@ def setup(self, *args, **kwargs) -> None: class ExternalLayerMapperViewMixin(ExternalLayerViewMixin): - """A mixin for views only available to users with mapper permission for the team responsible for the layer and/or Mapper team permission.""" + """A mixin for views. + + only available to users with mapper permission for the team responsible + for the layer and/or Mapper team permission. + """ - def setup(self, request, *args, **kwargs) -> None: + def setup(self, request: HttpRequest, *args, **kwargs) -> None: + """Check permissions.""" super().setup(request, *args, **kwargs) if ( self.layer.responsible_team diff --git a/src/maps/models.py b/src/maps/models.py index 6132b1f1a..5510f8b0b 100644 --- a/src/maps/models.py +++ b/src/maps/models.py @@ -1,6 +1,14 @@ +"""Maps models.""" + from __future__ import annotations import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from camps.models import Camp + +from typing import ClassVar from colorfield.fields import ColorField from django.contrib.auth.models import User @@ -27,6 +35,7 @@ class Group(UUIDModel): ) def __str__(self) -> str: + """String formatter.""" return str(self.name) @@ -73,13 +82,16 @@ class Layer(ExportModelOperationsMixin("layer"), UUIDModel): ) @property - def camp(self): + def camp(self) -> Camp: + """Camp object reference.""" return self.responsible_team.camp def __str__(self) -> str: + """String formatter.""" return str(self.name) def save(self, **kwargs) -> None: + """Set slug and save.""" self.slug = unique_slugify( str(self.name), slugs_in_use=self.__class__.objects.all().values_list( @@ -134,7 +146,9 @@ class Feature(UUIDModel): ) class Meta: - constraints = [ + """Meta data.""" + + constraints: ClassVar[list] = [ models.UniqueConstraint( fields=["layer", "name"], name="layer_and_name_uniq", @@ -142,14 +156,18 @@ class Meta: ] def __str__(self) -> str: + """String formatter.""" return str(self.name) @property - def camp(self): + def camp(self) -> Camp: + """Camp object reference.""" return self.layer.team.camp class ExternalLayer(UUIDModel): + """External layer model.""" + name = models.CharField( max_length=100, help_text="Name or description of this layer", @@ -177,13 +195,16 @@ class ExternalLayer(UUIDModel): ) @property - def camp(self): + def camp(self) -> Camp: + """Camp object reference.""" return self.responsible_team.camp def __str__(self) -> str: + """String formatter.""" return str(self.name) def save(self, **kwargs) -> None: + """Set slug and save.""" self.slug = unique_slugify( str(self.name), slugs_in_use=self.__class__.objects.all().values_list( @@ -195,6 +216,8 @@ def save(self, **kwargs) -> None: class UserLocationType(UUIDModel): + """User Location Type model.""" + name = models.CharField( max_length=100, help_text="Name of the user location type", @@ -220,9 +243,11 @@ class UserLocationType(UUIDModel): ) def __str__(self) -> str: + """String formatter.""" return self.name def save(self, **kwargs) -> None: + """Set slug and save.""" if not self.slug: self.slug = unique_slugify( self.name, @@ -238,6 +263,8 @@ class UserLocation( UUIDModel, CampRelatedModel, ): + """UserLocation model.""" + name = models.CharField( max_length=100, help_text="Name of the location", @@ -278,4 +305,5 @@ class UserLocation( ) def __str__(self) -> str: + """String formatter.""" return self.name diff --git a/src/maps/templates/maps_map.html b/src/maps/templates/maps_map.html index eb96ec08c..aa77384ba 100644 --- a/src/maps/templates/maps_map.html +++ b/src/maps/templates/maps_map.html @@ -22,7 +22,7 @@ {{ mapData|json_script:"mapData" }} - + {% endblock extra_head %} {% block content %} diff --git a/src/maps/tests.py b/src/maps/tests.py deleted file mode 100644 index c06c46b0e..000000000 --- a/src/maps/tests.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from unittest import mock - -from django.core.exceptions import PermissionDenied -from django.test import TestCase -from django.test import override_settings -from django.test.client import RequestFactory - -from .views import MapProxyView -from .views import MissingCredentials - -USER = "user" -PASSWORD = "password" - - -@override_settings( - DATAFORDELER_USER=USER, - DATAFORDELER_PASSWORD=PASSWORD, -) -class MapProxyViewTest(TestCase): - def setUp(self): - self.rf = RequestFactory() - - self.allowed_endpoints = [ - "/GeoDanmarkOrto/orto_foraar_wmts/1.0.0/WMTS", - "/Dkskaermkort/topo_skaermkort/1.0.0/wms", - "/DHMNedboer/dhm/1.0.0/wms", - ] - - def test_endpoint_not_allowed_raises_perm_denied(self): - fix_request = self.rf.get("/maps/kfproxy/not/allowed/endpoint") - - with self.assertRaises(PermissionDenied): - MapProxyView.as_view()(fix_request) - - def test_all_allowed_endpoints(self): - for endpoint in self.allowed_endpoints: - fix_request = self.rf.get("/maps/kfproxy" + endpoint) - with self.subTest(request=fix_request): - with mock.patch("maps.views.requests") as mock_req: - mock_req.get.return_value.status_code = 200 - result = MapProxyView.as_view()(fix_request) - - self.assertEqual(result.status_code, 200) - - def test_sanitizing_path(self): - fix_path = "/maps/kfproxy/DHMNedboer/dhm/1.0.0/wms?transparent=true" - - result = MapProxyView().sanitize_path(fix_path) - - self.assertEqual(result, "/DHMNedboer/dhm/1.0.0/wms?transparent=TRUE") - - def test_sanitizing_path_not_failing_without_query(self): - fix_path = "/maps/kfproxy/DHMNedboer/dhm/1.0.0/wms" - - result = MapProxyView().sanitize_path(fix_path) - - self.assertEqual(result, "/DHMNedboer/dhm/1.0.0/wms") - - def test_append_credentials(self): - fix_path = "/path" - fix_result = fix_path + f"&username={USER}&password={PASSWORD}" - - result = MapProxyView().append_credentials(fix_path) - - self.assertEqual(result, fix_result) - - def test_append_credentials_raises_perm_denied_if_no_creds_is_set(self): - with ( - self.settings( - DATAFORDELER_USER="", - DATAFORDELER_PASSWORD="", - ), - self.assertRaises(MissingCredentials), - ): - MapProxyView().append_credentials("path") diff --git a/src/maps/tests/__init__.py b/src/maps/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/maps/tests/test_views.py b/src/maps/tests/test_views.py new file mode 100644 index 000000000..0faa4efca --- /dev/null +++ b/src/maps/tests/test_views.py @@ -0,0 +1,222 @@ +"""Test cases for the Maps application.""" +from __future__ import annotations + +from unittest import mock + +from bs4 import BeautifulSoup +from django.contrib.gis.geos import Point +from django.core.exceptions import PermissionDenied +from django.test import TestCase +from django.test import override_settings +from django.test.client import RequestFactory +from django.urls import reverse + +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 utils.tests import BornhackTestBase + +USER = "user" +PASSWORD = "password" + + +@override_settings( + DATAFORDELER_USER=USER, + DATAFORDELER_PASSWORD=PASSWORD, +) +class MapProxyViewTest(TestCase): + """Test the Proxy view.""" + def setUp(self): + """Setup function.""" + self.rf = RequestFactory() + + self.allowed_endpoints = [ + "/GeoDanmarkOrto/orto_foraar_wmts/1.0.0/WMTS", + "/Dkskaermkort/topo_skaermkort/1.0.0/wms", + "/DHMNedboer/dhm/1.0.0/wms", + ] + + def test_endpoint_not_allowed_raises_perm_denied(self): + """Test endpoint not allowed.""" + fix_request = self.rf.get("/maps/kfproxy/not/allowed/endpoint") + + with self.assertRaises(PermissionDenied): + MapProxyView.as_view()(fix_request) + + def test_all_allowed_endpoints(self): + """Test allowed endpoints.""" + for endpoint in self.allowed_endpoints: + fix_request = self.rf.get("/maps/kfproxy" + endpoint) + with self.subTest(request=fix_request): + with mock.patch("maps.views.requests") as mock_req: + mock_req.get.return_value.status_code = 200 + result = MapProxyView.as_view()(fix_request) + + self.assertEqual(result.status_code, 200) + + def test_sanitizing_path(self): + """Test sanitization of paths.""" + fix_path = "/maps/kfproxy/DHMNedboer/dhm/1.0.0/wms?transparent=true" + + result = MapProxyView().sanitize_path(fix_path) + + self.assertEqual(result, "/DHMNedboer/dhm/1.0.0/wms?transparent=TRUE") + + def test_sanitizing_path_not_failing_without_query(self): + """Test sanitization of paths without query.""" + fix_path = "/maps/kfproxy/DHMNedboer/dhm/1.0.0/wms" + + result = MapProxyView().sanitize_path(fix_path) + + self.assertEqual(result, "/DHMNedboer/dhm/1.0.0/wms") + + def test_append_credentials(self): + """Test appending credentials.""" + fix_path = "/path" + fix_result = fix_path + f"&username={USER}&password={PASSWORD}" + + result = MapProxyView().append_credentials(fix_path) + + self.assertEqual(result, fix_result) + + def test_append_credentials_raises_perm_denied_if_no_creds_is_set(self): + """Test appending credentials exceptions.""" + with self.settings( + DATAFORDELER_USER="", + DATAFORDELER_PASSWORD="", + ), self.assertRaises(MissingCredentialsError): + MapProxyView().append_credentials("path") + +class MapsViewTest(BornhackTestBase): + """Test Maps View""" + + layer: Layer + group: Group + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + + # Create a layer + cls.group = Group(name="Test Group") + cls.group.save() + cls.layer = Layer( + name="Test layer 1", + slug="test_1", + description="Test Layer", + icon="fas fa-tractor", + group=cls.group, + responsible_team=cls.team, + ) + cls.layer.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 + url = reverse("maps:map_layer_geojson", kwargs={"layer_slug": "123test"}) + response = self.client.get(url) + assert response.status_code == 404 + + def test_map_views(self) -> None: + """Test the map view.""" + url = reverse("maps_map", kwargs={"camp_slug": self.camp.slug}) + response = self.client.get(url) + assert response.status_code == 200 + + def test_marker_views(self) -> None: + """Test the marker view.""" + good = ["ffffff", "ffffff00"] + bad = ["ffff", "qwerty"] + for color in good: + url = reverse("maps:marker", kwargs={"color": color}) + response = self.client.get(url) + assert response.status_code == 200 + + for color in bad: + url = reverse("maps:marker", kwargs={"color": color}) + response = self.client.get(url, raise_request_exception=True) + assert response.status_code == 400 + +class MapsUserLocationViewTest(BornhackTestBase): + """Test User Location Views""" + + user_location: UserLocation + user_location_type: UserLocationType + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + super().setUpTestData() + + #Create user location type + cls.user_location_type = UserLocationType( + name="Test Type", + slug="test", + icon="fas fa-tractor", + marker="blueIcon", + ) + cls.user_location_type.save() + + #Create user location + cls.user_location = UserLocation( + name="Test User Location", + type = cls.user_location_type, + camp=cls.camp, + user=cls.users[0], + location=Point([9.940218,55.388329]), + ) + cls.user_location.save() + + def test_user_location_geojson_view(self) -> None: + """Test the user location geojson view.""" + url = reverse("maps_user_location_layer", kwargs={ + "camp_slug": self.camp.slug, + "user_location_type_slug": self.user_location_type.slug, + }) + response = self.client.get(url) + assert response.status_code == 200 + + def test_user_location_view(self) -> None: + """Test the user location list view.""" + self.client.force_login(self.users[0]) + url = reverse("maps_user_location_list", kwargs={ + "camp_slug": self.camp.slug, + }) + response = self.client.get(url) + assert response.status_code == 200 + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div#main > table > tbody > tr") + self.assertEqual(len(rows), 1, "user location list does not return 1 entries") + + def test_user_location_create(self) -> None: + """Test the user location create view.""" + self.client.force_login(self.users[0]) + url = reverse("maps_user_location_create", kwargs={ + "camp_slug": self.camp.slug, + }) + response = self.client.post( + path=url, + data={ + "name": "Test User Location Create", + "type": self.user_location_type.pk, + "location": '{"type":"Point","coordinates":[9.940218,55.388329]}', + }, + follow=True, + ) + assert response.status_code == 200 + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div#main > table > tbody > tr") + self.assertEqual(len(rows), 2, "user location list does not return 2 entries after create") diff --git a/src/maps/urls.py b/src/maps/urls.py index 0529ea295..f00104982 100644 --- a/src/maps/urls.py +++ b/src/maps/urls.py @@ -1,3 +1,5 @@ +"""Maps URLS File.""" + from __future__ import annotations from django.urls import include @@ -7,12 +9,10 @@ from .views import LayerGeoJSONView from .views import MapMarkerView from .views import MapProxyView -from .views import MapView app_name = "maps" urlpatterns = [ - path("map/", MapView.as_view(), name="map"), path("marker//", MapMarkerView.as_view(), name="marker"), path( "/", diff --git a/src/maps/utils.py b/src/maps/utils.py index cd5c1deff..510e502a7 100644 --- a/src/maps/utils.py +++ b/src/maps/utils.py @@ -1,10 +1,14 @@ +"""Utils for the Maps APP.""" + from __future__ import annotations from django.db import models class LeafletMarkerChoices(models.TextChoices): - """Leaflet icon color choices, a models.TextChoices class to use when we want to set + """Leaflet icon color choices. + + a models.TextChoices class to use when we want to set choices for a model field to pick a marker colour for a Leaflet map. These map directly to the L.Icon() objects in static_src/js/leaflet-color-markers.js. """ diff --git a/src/maps/views.py b/src/maps/views.py index 5e718add7..5ae9c917e 100644 --- a/src/maps/views.py +++ b/src/maps/views.py @@ -1,17 +1,22 @@ +"""Maps view.""" + from __future__ import annotations import json import logging import re +from typing import TYPE_CHECKING import requests from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.gis.geos import Point +from django.core.exceptions import BadRequest from django.core.exceptions import PermissionDenied from django.core.serializers import serialize from django.db.models import Q +from django.http import HttpRequest from django.http import HttpResponse from django.http import HttpResponseNotAllowed from django.shortcuts import get_object_or_404 @@ -30,6 +35,13 @@ from leaflet.forms.widgets import LeafletWidget from oauth2_provider.views.generic import ScopedProtectedResourceView +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.forms import BaseForm + from django.http import TemplateResponse + +from typing import ClassVar + from camps.mixins import CampViewMixin from facilities.models import FacilityType from utils.color import adjust_color @@ -45,9 +57,18 @@ logger = logging.getLogger(f"bornhack.{__name__}") +class MissingCredentialsError(Exception): + """Missing Credentials Exception.""" + + +class MarkerColorError(ValueError): + """Exception raised on invalid color.""" -class MissingCredentials(Exception): - pass + def __init__(self) -> None: + """Exception raised on invalid color.""" + error = "Hex color must be in format RRGGBB or RRGGBBAA" + logger.exception(error) + super().__init__(error) class MapMarkerView(TemplateView): @@ -56,27 +77,39 @@ class MapMarkerView(TemplateView): template_name = "marker.svg" @property - def color(self): + def color(self) -> tuple: + """Return the color values as ints.""" hex_color = self.kwargs["color"] length = len(hex_color) - if length == 6: # RGB - r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + if length == 6: # RGB # noqa: PLR2004 + try: + r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + except ValueError as e: + raise MarkerColorError from e return (r, g, b) - if length == 8: # RGBA - r, g, b, a = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6)) + if length == 8: # RGBA # noqa: PLR2004 + try: + r, g, b, a = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6)) + except ValueError as e: + raise MarkerColorError from e return (r, g, b, a) - raise ValueError("Hex color must be in format RRGGBB or RRGGBBAA") + raise MarkerColorError - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Get the context data.""" context = super().get_context_data(**kwargs) - context["stroke1"] = self.color - context["stroke0"] = adjust_color(self.color, -0.4) if is_dark(self.color) else adjust_color(self.color) - context["fill0"] = adjust_color(self.color, -0.4) if is_dark(self.color) else adjust_color(self.color) - context["fill1"] = self.color + try: + context["stroke0"] = adjust_color(self.color, -0.4) if is_dark(self.color) else adjust_color(self.color) + context["stroke1"] = self.color + context["fill0"] = adjust_color(self.color, -0.4) if is_dark(self.color) else adjust_color(self.color) + context["fill1"] = self.color + except MarkerColorError as e: + raise BadRequest from e return context - def render_to_response(self, context, **kwargs): + def render_to_response(self, context: dict, **kwargs) -> TemplateResponse: + """Render the SVG output.""" return super().render_to_response( context, content_type="image/svg+xml", @@ -90,7 +123,8 @@ class MapView(CampViewMixin, TemplateView): template_name = "maps_map.html" context_object_name = "maps_map" - def get_context_data(self, **kwargs): + 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, @@ -119,7 +153,7 @@ def get_context_data(self, **kwargs): ), "externalLayers": list(context["externalLayers"].values()), "villages": reverse( - "villages_geojson", + "villages:villages_geojson", kwargs={"camp_slug": self.camp.slug}, ), "user_location_types": list( @@ -155,7 +189,8 @@ def get_context_data(self, **kwargs): class LayerGeoJSONView(LayerViewMixin, JsonView): """GeoJSON export view.""" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Return the GeoJSON Data to the client.""" return json.loads( serialize( "geojson", @@ -175,20 +210,22 @@ def get_context_data(self, **kwargs): class MapProxyView(View): - """Proxy for Datafordeler map service. Created so we can show maps without - leaking the IP of our visitors. + """Proxy for Datafordeler map service. + + Created so we can show maps without leaking the IP of our visitors. """ PROXY_URL = "/maps/kfproxy" - VALID_ENDPOINTS = [ + VALID_ENDPOINTS: ClassVar[list[str]] = [ "/GeoDanmarkOrto/orto_foraar_wmts/1.0.0/WMTS", "/GeoDanmarkOrto/orto_foraar/1.0.0/WMS", "/Dkskaermkort/topo_skaermkort/1.0.0/wms", "/DHMNedboer/dhm/1.0.0/wms", ] - def get(self, *args, **kwargs): + def get(self, *args, **kwargs) -> HttpResponse: """Before we make the request we check that the path is in our whitelist. + Before we return the response we copy headers except for a list we dont want. """ # Raise PermissionDenied if endpoint isn't valid @@ -201,7 +238,7 @@ def get(self, *args, **kwargs): path = self.append_credentials(path) # make the request - r = requests.get("https://services.datafordeler.dk" + path) + r = requests.get("https://services.datafordeler.dk" + path, timeout=10) # make the response response = HttpResponse(r.content, status=r.status_code) @@ -237,7 +274,7 @@ def is_endpoint_valid(self, path: str) -> None: endpoint, self.VALID_ENDPOINTS, ) - raise PermissionDenied("No thanks") + raise PermissionDenied def sanitize_path(self, path: str) -> str: """Sanitize path by removing PROXY_URL and set 'transparent' value to upper.""" @@ -256,7 +293,7 @@ def append_credentials(self, path: str) -> str: logger.error( "Missing credentials for 'DATAFORDELER_USER' or 'DATAFORDELER_PASSWORD'", ) - raise MissingCredentials + raise MissingCredentialsError path += f"&username={username}&password={password}" return path @@ -267,7 +304,8 @@ def append_credentials(self, path: str) -> str: class UserLocationLayerView(CampViewMixin, JsonView): """UserLocation geojson view.""" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Get context data.""" context = {} context["type"] = "FeatureCollection" context["features"] = self.dump_locations() @@ -275,37 +313,38 @@ def get_context_data(self, **kwargs): def dump_locations(self) -> list[object]: """GeoJSON Formatter.""" - output = [] - for location in UserLocation.objects.filter( - camp=self.camp, - type__slug=self.kwargs["user_location_type_slug"], - ): - output.append( - { - "type": "Feature", - "id": location.pk, - "geometry": { - "type": "Point", - "coordinates": [location.location.x, location.location.y], - }, - "properties": { - "name": location.name, - "type": location.type.name, - "icon": location.type.icon, - "marker": location.type.marker, - "user": location.user.profile.get_public_credit_name, - "data": location.data, - }, + return [ + { + "type": "Feature", + "id": location.pk, + "geometry": { + "type": "Point", + "coordinates": [location.location.x, location.location.y], + }, + "properties": { + "name": location.name, + "type": location.type.name, + "icon": location.type.icon, + "marker": location.type.marker, + "user": location.user.profile.get_public_credit_name, + "data": location.data, }, + } + for location in UserLocation.objects.filter( + camp=self.camp, + type__slug=self.kwargs["user_location_type_slug"], ) - return output + ] class UserLocationListView(LoginRequiredMixin, CampViewMixin, ListView): + """UserLocation view.""" + template_name = "user_location_list.html" model = UserLocation - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Get data for the view.""" context = super().get_context_data(**kwargs) context["user_location_types"] = UserLocationType.objects.all().values_list( "slug", @@ -313,19 +352,25 @@ def get_context_data(self, **kwargs): ) return context - def get_queryset(self, *args, **kwargs): + def get_queryset(self, *args, **kwargs) -> QuerySet: """Show only entries belonging to the current user.""" qs = super().get_queryset(*args, **kwargs) return qs.filter(user=self.request.user) class UserLocationCreateView(LoginRequiredMixin, CampViewMixin, CreateView): + """Create view for UserLocation.""" + model = UserLocation template_name = "user_location_form.html" - fields = ["name", "type", "location", "data"] + fields: ClassVar[list[str]] = ["name", "type", "location", "data"] - def dispatch(self, *args, **kwargs): - if UserLocation.objects.filter(user=self.request.user, camp=self.camp).count() > 49: + def dispatch(self, *args, **kwargs) -> str: + """Check user limits.""" + if ( + UserLocation.objects.filter(user=self.request.user, camp=self.camp).count() + >= settings.MAPS_USER_LOCATION_MAX + ): messages.error( self.request, "To many User Locations (50), please delete some.", @@ -338,7 +383,8 @@ def dispatch(self, *args, **kwargs): ) return super().dispatch(*args, **kwargs) - def get_form(self, *args, **kwargs): + def get_form(self, *args, **kwargs) -> BaseForm: + """Prepare the form.""" form = super().get_form(*args, **kwargs) form.fields["location"].widget = LeafletWidget( attrs={ @@ -350,7 +396,8 @@ def get_form(self, *args, **kwargs): ) return form - def form_valid(self, form): + def form_valid(self, form: BaseForm) -> str: + """Check if the form is valid.""" location = form.save(commit=False) location.camp = self.camp location.user = self.request.user @@ -373,18 +420,22 @@ class UserLocationUpdateView( UserIsObjectOwnerMixin, UpdateView, ): + """Update view for UserLocation.""" + model = UserLocation template_name = "user_location_form.html" - fields = ["name", "type", "location", "data"] + fields: ClassVar[list[str]] = ["name", "type", "location", "data"] slug_url_kwarg = "user_location" slug_field = "pk" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Get the context data for the view.""" context = super().get_context_data(**kwargs) context["mapData"] = {"grid": static("json/grid.geojson")} return context - def get_form(self, *args, **kwargs): + def get_form(self, *args, **kwargs) -> BaseForm: + """get_form preparing the form.""" form = super().get_form(*args, **kwargs) form.fields["location"].widget = LeafletWidget( attrs={ @@ -396,7 +447,8 @@ def get_form(self, *args, **kwargs): ) return form - def get_success_url(self): + def get_success_url(self) -> str: + """Produce the success url.""" return reverse( "maps_user_location_list", kwargs={"camp_slug": self.camp.slug}, @@ -409,12 +461,15 @@ class UserLocationDeleteView( UserIsObjectOwnerMixin, DeleteView, ): + """Delete view for UserLocation.""" + model = UserLocation template_name = "user_location_delete.html" slug_url_kwarg = "user_location" slug_field = "pk" - def get_success_url(self): + def get_success_url(self) -> str: + """Produce the success url.""" messages.success( self.request, f"Your User Location {self.get_object().name} has been deleted successfully.", @@ -433,9 +488,10 @@ class UserLocationApiView( ): """This view has 2 endpoints /create/api (POST) AND //api (GET, PATCH, DELETE).""" - required_scopes = ["location:write"] + required_scopes: ClassVar[list[str]] = ["location:write"] - def get(self, request, **kwargs): + def get(self, request: HttpRequest, **kwargs) -> dict: + """HTTP Method for viewing a user location.""" if "user_location" not in kwargs: return HttpResponseNotAllowed(permitted_methods=["POST"]) location = get_object_or_404( @@ -451,10 +507,11 @@ def get(self, request, **kwargs): "data": location.data, } - def post(self, request, **kwargs): + def post(self, request: HttpRequest, **kwargs) -> dict: + """HTTP Method for creating a user location.""" if "user_location" in kwargs: return HttpResponseNotAllowed(permitted_methods=["GET", "PATCH", "DELETE"]) - if UserLocation.objects.filter(user=request.user, camp=self.camp).count() > 49: + if UserLocation.objects.filter(user=request.user, camp=self.camp).count() >= settings.MAPS_USER_LOCATION_MAX: return {"error": "To many user locations created (50)"} data = json.loads(request.body) try: @@ -481,7 +538,8 @@ def post(self, request, **kwargs): "data": location.data, } - def patch(self, request, **kwargs): + def patch(self, request: HttpRequest, **kwargs) -> dict: + """HTTP Method for updating a user location.""" if "user_location" not in kwargs and "camp_slug" not in kwargs: return HttpResponseNotAllowed(permitted_methods=["POST"]) location = get_object_or_404( @@ -513,7 +571,8 @@ def patch(self, request, **kwargs): "data": location.data, } - def delete(self, request, **kwargs): + def delete(self, request: HttpRequest, **kwargs) -> dict: + """HTTP Method for deleting a user location.""" if "user_location" not in kwargs and "camp_slug" not in kwargs: return HttpResponseNotAllowed(permitted_methods=["POST"]) location = get_object_or_404( diff --git a/src/utils/tests.py b/src/utils/tests.py index f5199512c..98b959807 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -8,12 +8,13 @@ import pytz from django.contrib.auth.models import User +from django.contrib.auth.models import Group from django.core.management import call_command from django.test import Client from django.test import TestCase from camps.models import Camp - +from teams.models import Team class TestBootstrapScript(TestCase): """Test bootstrap_devsite script (touching many codepaths)""" @@ -29,6 +30,7 @@ class BornhackTestBase(TestCase): users: list[User] camp: Camp + team: Team @classmethod def setUpTestData(cls) -> None: @@ -70,3 +72,17 @@ def setUpTestData(cls) -> None: user.set_password("user0") user.save() cls.users.append(user) + + #Create a team + team_group = Group(name="Test Team Group") + team_group.save() + cls.team = Team( + camp=cls.camp, + name="Test Team", + group=team_group, + slug="test", + shortslug="test", + description="Many test Such Team", + needs_members=True, + ) + cls.team.save()