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()