diff --git a/docker-compose.yml b/docker-compose.yml index 3e13981a..0a692966 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ --- -version: '3' services: runserver: user: bew diff --git a/docs/api/enforcement.yaml b/docs/api/enforcement.yaml index 4f684ea8..ee23d1fa 100644 --- a/docs/api/enforcement.yaml +++ b/docs/api/enforcement.yaml @@ -105,7 +105,7 @@ components: time_start: "2020-04-15T11:10:05Z" time_end: "2020-04-15T14:00:00Z" operator: "63f8374e-1eed-44fa-9224-4cd0c64ddf35" - operator_name: "Foobar Eent Parkings" + operator_name: "Foobar Event Parkings" event_area: "cdb91cd2-88b0-4869-9c93-4df4fad28a33" properties: id: @@ -445,6 +445,13 @@ paths: type: string maxLength: 10 nullable: true + event_area: + description: >- + Event Area found by the provided location, + or null if the provided location is not in any + event area in the system. + type: string + nullable: true time: description: >- Time used in the check. It will either be equal diff --git a/parkings/api/enforcement/check_parking.py b/parkings/api/enforcement/check_parking.py index b85c19b5..12499717 100644 --- a/parkings/api/enforcement/check_parking.py +++ b/parkings/api/enforcement/check_parking.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from ...models import ( - EventParking, Parking, ParkingCheck, PaymentZone, PermitArea, + EventArea, EventParking, Parking, ParkingCheck, PaymentZone, PermitArea, PermitLookupItem) from ...models.constants import GK25FIN_SRID, WGS84_SRID from .permissions import IsEnforcer @@ -60,10 +60,10 @@ def post(self, request): zone = get_payment_zone(gk25_location, domain) area = get_permit_area(gk25_location, domain) + event_area = get_event_area(gk25_location, domain) (allowed_by, parking, end_time) = check_parking( - registration_number, zone, area, time, domain) - + registration_number, zone, area, time, domain, event_area) allowed = bool(allowed_by) if not allowed: @@ -72,7 +72,7 @@ def post(self, request): # ago (where "a few minutes" is the grace duration) past_time = time - get_grace_duration() (_allowed_by, parking, end_time) = check_parking( - registration_number, zone, area, past_time, domain) + registration_number, zone, area, past_time, domain, event_area) result = { "allowed": allowed, @@ -80,6 +80,7 @@ def post(self, request): "location": { "payment_zone": zone, "permit_area": area.identifier if area else None, + "event_area": event_area.id if event_area else None, }, "time": time, } @@ -132,15 +133,16 @@ def get_permit_area(location, domain): area = PermitArea.objects.filter(geom__contains=location, domain=domain).first() return area if area else None -# def get_event_area(location, domain): -# if location is None: -# return None -# now = timezone.now() -# area = EventArea.objects.filter(geom__contains=location, domain=domain, time_end__gte=now).first() -# return area if area else None + +def get_event_area(location, domain): + if location is None: + return None + now = timezone.now() + area = EventArea.objects.filter(geom__contains=location, domain=domain, time_end__gte=now).first() + return area if area else None -def check_parking(registration_number, zone, area, time, domain): +def check_parking(registration_number, zone, area, time, domain, event_area): """ Check parking allowance from the database. @@ -184,7 +186,11 @@ def check_parking(registration_number, zone, area, time, domain): .filter(domain=domain)).first() if active_event_parking: - return ("event parking", active_event_parking, active_event_parking.time_end) + if active_event_parking.event_area == event_area: + return ("event parking", active_event_parking, active_event_parking.time_end) + else: + # Event parking not parked in the assigned event area, i.e., not allowed. + return (None, active_event_parking, active_event_parking.time_end) return (None, None, None) diff --git a/parkings/api/enforcement/utils.py b/parkings/api/enforcement/utils.py index 4827634f..1ca4d4d9 100644 --- a/parkings/api/enforcement/utils.py +++ b/parkings/api/enforcement/utils.py @@ -2,8 +2,18 @@ from django.conf import settings +from .check_parking import get_event_area + def get_grace_duration(default=datetime.timedelta(minutes=15)): value = getattr(settings, 'PARKKIHUBI_TIME_OLD_PARKINGS_VISIBLE', None) assert value is None or isinstance(value, datetime.timedelta) return value if value is not None else default + + +def get_event_parkings_in_assigned_event_areas(queryset): + in_assigned_event_area = [] + for event_parking in queryset.all(): + if get_event_area(event_parking.location_gk25fin, domain=event_parking.domain) == event_parking.event_area: + in_assigned_event_area.append(event_parking.id) + return queryset.filter(id__in=in_assigned_event_area) diff --git a/parkings/api/enforcement/valid_parking.py b/parkings/api/enforcement/valid_parking.py index ea008603..8918b4d2 100644 --- a/parkings/api/enforcement/valid_parking.py +++ b/parkings/api/enforcement/valid_parking.py @@ -6,7 +6,8 @@ from ...models import Parking from .permissions import IsEnforcer -from .utils import get_grace_duration +from .utils import ( + get_event_parkings_in_assigned_event_areas, get_grace_duration) class ValidSerializer(serializers.ModelSerializer): @@ -123,6 +124,7 @@ class Meta: class ValidViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsEnforcer] + model = None class Meta: abstract = True @@ -140,7 +142,6 @@ def filter_queryset(self, queryset): with the PARKKIHUBI_TIME_OLD_PARKINGS_VISIBLE setting. """ filtered_queryset = super().filter_queryset(queryset) - if filtered_queryset: return filtered_queryset @@ -173,7 +174,10 @@ def _get_filterset(self, queryset): return filter_backend.get_filterset(self.request, queryset, self) def get_queryset(self): - return super().get_queryset().filter(domain=self.request.user.enforcer.enforced_domain) + queryset = super().get_queryset().filter(domain=self.request.user.enforcer.enforced_domain) + if self.__class__.__name__ == "ValidEventParkingViewSet": + queryset = queryset.filter(id__in=get_event_parkings_in_assigned_event_areas(self.queryset)) + return queryset class ValidParkingViewSet(ValidViewSet): diff --git a/parkings/api/monitoring/valid_parking.py b/parkings/api/monitoring/valid_parking.py index 4f5a8bed..9d30bf7d 100644 --- a/parkings/api/monitoring/valid_parking.py +++ b/parkings/api/monitoring/valid_parking.py @@ -6,6 +6,7 @@ from ...models import Parking from ..common import WGS84InBBoxFilter +from ..enforcement.utils import get_event_parkings_in_assigned_event_areas from .permissions import IsMonitor from .serializers import ParkingSerializer @@ -42,7 +43,10 @@ class ValidViewSet(viewsets.ReadOnlyModelViewSet): bbox_filter_include_overlapping = True def get_queryset(self): - return super().get_queryset().filter(domain=self.request.user.monitor.domain) + queryset = super().get_queryset().filter(domain=self.request.user.monitor.domain) + if self.__class__.__name__ == "ValidEventParkingViewSet": + queryset = queryset.filter(id__in=get_event_parkings_in_assigned_event_areas(self.queryset)) + return queryset class Meta: abstract = True diff --git a/parkings/tests/api/enforcement/test_check_parking.py b/parkings/tests/api/enforcement/test_check_parking.py index bffa1b28..f6a2ea8b 100644 --- a/parkings/tests/api/enforcement/test_check_parking.py +++ b/parkings/tests/api/enforcement/test_check_parking.py @@ -28,6 +28,12 @@ "location": {"longitude": 24.9, "latitude": 60.2}, } +PARKING_DATA_2 = { + "registration_number": "ABC-123", + "location": {"longitude": 26.4, "latitude": 64.2}, +} + + INVALID_PARKING_DATA = { "registration_number": "ABC-123", "location": {"longitude": 24.9, "latitude": 60.4}, @@ -91,21 +97,23 @@ def create_permit(domain, permit_series=None, end_time=None): ) -def test_check_parking_allowed_event_parking(enforcer_api_client, event_parking_factory, enforcer): - event_parking = event_parking_factory(registration_number="ABC-123", domain=enforcer.enforced_domain) +def test_check_parking_allowed_event_parking(enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + event_parking = event_parking_factory(registration_number="ABC-123", + domain=enforcer.enforced_domain, event_area=event_area) response = enforcer_api_client.post(list_url, data=PARKING_DATA) assert response.status_code == HTTP_200_OK assert response.data["allowed"] is True assert response.data["end_time"] == event_parking.time_end - assert ParkingCheck.objects.filter( registration_number=event_parking.registration_number).first().result["allowed"] is True -def test_check_parking_not_allowed_event_parking(enforcer_api_client, event_parking_factory, enforcer): - event_parking_factory(registration_number="CBA-123", domain=enforcer.enforced_domain) +def test_check_parking_not_allowed_event_parking( + enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + event_parking_factory(registration_number="CBA-123", domain=enforcer.enforced_domain, event_area=event_area) response = enforcer_api_client.post(list_url, data=PARKING_DATA) - assert response.status_code == HTTP_200_OK assert response.data["allowed"] is False assert response.data["end_time"] is None @@ -114,18 +122,52 @@ def test_check_parking_not_allowed_event_parking(enforcer_api_client, event_park registration_number=PARKING_DATA["registration_number"]).first().result["allowed"] is False -def test_check_event_parking_with_time_end_null(enforcer_api_client, event_parking_factory, enforcer): +def test_check_event_parking_parked_in_wrong_event_area( + enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area_1 = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + event_area_2 = event_area_factory.create(geom=create_area_geom(geom=GEOM_2), domain=enforcer.enforced_domain) + # Parked inside event_area_2, i.e., inside GEOM_2, but event_area_1 is assigned + location = Point(PARKING_DATA_2["location"]["longitude"], PARKING_DATA_2["location"]["latitude"], srid=WGS84_SRID) + event_parking = event_parking_factory(registration_number="ABC-123", + domain=enforcer.enforced_domain, event_area=event_area_1, location=location) + response = enforcer_api_client.post(list_url, data=PARKING_DATA_2) + assert response.status_code == HTTP_200_OK + assert response.data["location"]["event_area"] == event_area_2.id + assert response.data["allowed"] is False + assert response.data["end_time"] == event_parking.time_end + assert ParkingCheck.objects.filter( + registration_number=PARKING_DATA["registration_number"]).first().result["allowed"] is False + + +def test_check_event_parking_outside_event_area( + enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + location = Point(PARKING_DATA_2["location"]["longitude"], PARKING_DATA_2["location"]["latitude"], srid=WGS84_SRID) + event_parking = event_parking_factory(registration_number="ABC-123", + domain=enforcer.enforced_domain, event_area=event_area, location=location) + response = enforcer_api_client.post(list_url, data=PARKING_DATA_2) + assert response.status_code == HTTP_200_OK + assert response.data["location"]["event_area"] is None + assert response.data["allowed"] is False + assert response.data["end_time"] == event_parking.time_end + assert ParkingCheck.objects.filter( + registration_number=PARKING_DATA["registration_number"]).first().result["allowed"] is False + + +def test_check_event_parking_with_time_end_null( + enforcer_api_client, event_parking_factory, enforcer, event_area_factory): time_end = None now = timezone.now() time_start = now - timedelta(hours=1) + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) event_parking_factory(registration_number="ABC-123", domain=enforcer.enforced_domain, - time_start=time_start, time_end=time_end) + time_start=time_start, time_end=time_end, event_area=event_area) response = enforcer_api_client.post(list_url, data=PARKING_DATA) assert response.status_code == HTTP_200_OK assert response.data["allowed"] is True assert response.data["end_time"] is None - + assert response.data["location"]["event_area"] == event_area.id assert ParkingCheck.objects.filter( registration_number=PARKING_DATA["registration_number"]).first().result["allowed"] is True @@ -195,7 +237,7 @@ def test_check_parking_invalid_location(enforcer_api_client, staff_user): assert response.status_code == HTTP_200_OK assert response.data["location"] == { - 'payment_zone': None, 'permit_area': None} + 'event_area': None, 'payment_zone': None, 'permit_area': None} assert response.data["allowed"] is False @@ -208,7 +250,7 @@ def test_returned_data_has_correct_schema(enforcer_api_client): assert isinstance(data["allowed"], bool) assert data["end_time"] is None assert isinstance(data["location"], dict) - assert sorted(data["location"].keys()) == ["payment_zone", "permit_area"] + assert sorted(data["location"].keys()) == ["event_area", "payment_zone", "permit_area"] assert isinstance(data["time"], datetime.datetime) diff --git a/parkings/tests/api/enforcement/test_valid_event_parking.py b/parkings/tests/api/enforcement/test_valid_event_parking.py index e1e853b4..e41ee139 100644 --- a/parkings/tests/api/enforcement/test_valid_event_parking.py +++ b/parkings/tests/api/enforcement/test_valid_event_parking.py @@ -1,6 +1,7 @@ from datetime import datetime import pytest +from django.contrib.gis.geos import Point from django.test import override_settings from django.urls import reverse from django.utils.timezone import utc @@ -10,6 +11,8 @@ from ..utils import ( ALL_METHODS, check_list_endpoint_base_fields, check_method_status_codes, check_response_objects, get, get_ids_from_results) +from .test_check_parking import ( + PARKING_DATA, PARKING_DATA_2, WGS84_SRID, create_area_geom) list_url = reverse('enforcement:v1:valid_event_parking-list') @@ -76,7 +79,9 @@ def test_list_endpoint_base_fields(enforcer_api_client): check_list_endpoint_base_fields(event_parking_data) -def test_list_endpoint_data(enforcer_api_client, event_parking, enforcer): +def test_list_endpoint_data(enforcer_api_client, event_parking, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + event_parking.event_area = event_area event_parking.domain = enforcer.enforced_domain event_parking.save() @@ -123,11 +128,33 @@ def iso8601_us(dt): return dt.strftime('%Y-%m-%dT%H:%M:%S.%fZ') -def test_registration_number_filter(operator, enforcer_api_client, event_parking_factory, enforcer): - p1 = event_parking_factory(registration_number='ABC-123', operator=operator, domain=enforcer.enforced_domain) - p2 = event_parking_factory(registration_number='ZYX-987', operator=operator, domain=enforcer.enforced_domain) - p3 = event_parking_factory(registration_number='ZYX-987', operator=operator, domain=enforcer.enforced_domain) - p4 = event_parking_factory(registration_number='Zyx987 ', operator=operator, domain=enforcer.enforced_domain) +def test_event_parkings_outside_event_area( + operator, enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + location = Point(PARKING_DATA_2["location"]["longitude"], PARKING_DATA_2["location"]["latitude"], srid=WGS84_SRID) + event_parking = event_parking_factory(registration_number='ABC-123', operator=operator, + domain=enforcer.enforced_domain, location=location, event_area=event_area) + results = get(enforcer_api_client, list_url_for('ABC-123'))['results'] + assert results == [] + # Test with location inside the event area + location = Point(PARKING_DATA["location"]["longitude"], PARKING_DATA["location"]["latitude"], srid=WGS84_SRID) + event_parking.location = location + event_parking.save() + results = get(enforcer_api_client, list_url_for('ABC-123'))['results'] + assert len(results) == 1 + check_event_parking_data_keys(results[0]) + + +def test_registration_number_filter(operator, enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + p1 = event_parking_factory(registration_number='ABC-123', operator=operator, + domain=enforcer.enforced_domain, event_area=event_area) + p2 = event_parking_factory(registration_number='ZYX-987', operator=operator, + domain=enforcer.enforced_domain, event_area=event_area) + p3 = event_parking_factory(registration_number='ZYX-987', operator=operator, + domain=enforcer.enforced_domain, event_area=event_area) + p4 = event_parking_factory(registration_number='Zyx987 ', operator=operator, + domain=enforcer.enforced_domain, event_area=event_area) results = get(enforcer_api_client, list_url_for('ABC-123'))['results'] assert get_ids_from_results(results) == {p1.id} @@ -157,20 +184,23 @@ def test_registration_number_filter(operator, enforcer_api_client, event_parking 'more_than_day_after_2nd', 'now', ]) -def test_time_filtering(operator, enforcer_api_client, event_parking_factory, name, enforcer): +def test_time_filtering(operator, enforcer_api_client, event_parking_factory, name, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) p1 = event_parking_factory( registration_number='ABC-123', time_start=datetime(2012, 1, 1, 12, 0, 0, tzinfo=utc), time_end=datetime(2014, 1, 1, 12, 0, 0, tzinfo=utc), operator=operator, - domain=enforcer.enforced_domain) + domain=enforcer.enforced_domain, + event_area=event_area) p2 = event_parking_factory( registration_number='ABC-123', time_start=datetime(2014, 1, 1, 12, 0, 0, tzinfo=utc), time_end=datetime(2016, 1, 1, 12, 0, 0, tzinfo=utc), operator=operator, - domain=enforcer.enforced_domain) - p3 = event_parking_factory(registration_number='ABC-123', domain=enforcer.enforced_domain) + domain=enforcer.enforced_domain, + event_area=event_area) + p3 = event_parking_factory(registration_number='ABC-123', domain=enforcer.enforced_domain, event_area=event_area) (time, expected_parkings) = { 'before_all': ('2000-01-01T12:00:00Z', []), @@ -194,7 +224,9 @@ def test_time_filtering(operator, enforcer_api_client, event_parking_factory, na @override_settings(PARKKIHUBI_NONE_END_TIME_REPLACEMENT='2030-12-31T23:59:59Z') -def test_null_time_end_is_replaced_correctly(enforcer_api_client, event_parking, enforcer): +def test_null_time_end_is_replaced_correctly(enforcer_api_client, event_parking, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + event_parking.event_area = event_area event_parking.time_end = None event_parking.domain = enforcer.enforced_domain event_parking.save() @@ -206,9 +238,11 @@ def test_null_time_end_is_replaced_correctly(enforcer_api_client, event_parking, def test_enforcer_can_view_only_parkings_from_domain_they_enforce( - enforcer_api_client, event_parking_factory, enforcer): - visible_parking = event_parking_factory(registration_number='ABC-123', domain=enforcer.enforced_domain) - event_parking_factory(registration_number='ABC-123') # Parking belonging to different domain + enforcer_api_client, event_parking_factory, enforcer, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=enforcer.enforced_domain) + visible_parking = event_parking_factory(registration_number='ABC-123', + domain=enforcer.enforced_domain, event_area=event_area) + event_parking_factory(registration_number='ABC-123', event_area=event_area) # Parking belonging to different domain response = get(enforcer_api_client, list_url_for('ABC-123')) diff --git a/parkings/tests/api/monitoring/test_valid_event_parking.py b/parkings/tests/api/monitoring/test_valid_event_parking.py index 86d6715c..4ac1e266 100644 --- a/parkings/tests/api/monitoring/test_valid_event_parking.py +++ b/parkings/tests/api/monitoring/test_valid_event_parking.py @@ -1,9 +1,14 @@ import pytest +from django.contrib.gis.geos import Point from django.urls import reverse from django.utils import timezone from rest_framework.status import ( HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_405_METHOD_NOT_ALLOWED) +from parkings.api.monitoring.region import WGS84_SRID + +from ..enforcement.test_check_parking import ( + GEOM_2, PARKING_DATA, PARKING_DATA_2, create_area_geom) from ..utils import ALL_METHODS, check_method_status_codes list_url = reverse('monitoring:v1:valid_event_parking-list') @@ -32,7 +37,9 @@ def test_disallowed_methods(monitoring_api_client, event_parking, kind): monitoring_api_client, [url], methods, HTTP_405_METHOD_NOT_ALLOWED) -def test_list_endpoint_data(monitoring_api_client, event_parking): +def test_list_endpoint_data(monitoring_api_client, event_parking, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=monitoring_api_client.monitor.domain) + event_parking.event_area = event_area event_parking.domain = monitoring_api_client.monitor.domain event_parking.save() @@ -50,6 +57,26 @@ def test_list_endpoint_data(monitoring_api_client, event_parking): check_parking_feature_matches_parking_object(parking_feature, event_parking) +def test_list_endpoint_event_parking_not_in_assigned_event_area( + monitoring_api_client, event_parking, event_area_factory): + event_area = event_area_factory.create(geom=create_area_geom(), domain=monitoring_api_client.monitor.domain) + location = Point(PARKING_DATA_2["location"]["longitude"], PARKING_DATA_2["location"]["latitude"], srid=WGS84_SRID) + event_parking.location = location + event_parking.domain = monitoring_api_client.monitor.domain + event_parking.event_area = event_area + event_parking.save() + result = monitoring_api_client.get(list_url, data={'time': event_parking.time_start.isoformat()}) + assert result.data["features"] == [] + # # Test with location inside the event area + location = Point(PARKING_DATA["location"]["longitude"], PARKING_DATA["location"]["latitude"], srid=WGS84_SRID) + event_parking.location = location + event_parking.save() + result = monitoring_api_client.get(list_url, data={'time': event_parking.time_start.isoformat()}) + parking_feature = result.data['features'][0] + check_parking_feature_shape(parking_feature) + check_parking_feature_matches_parking_object(parking_feature, event_parking) + + def check_parking_feature_shape(parking_feature): assert set(parking_feature.keys()) == { 'id', 'type', 'geometry', 'properties'} @@ -91,12 +118,18 @@ def iso8601_us(dt): def test_monitor_can_view_only_parkings_from_their_domain( - monitoring_api_client, staff_api_client, staff_user, event_parking_factory, monitor_factory + monitoring_api_client, staff_api_client, staff_user, event_parking_factory, monitor_factory, event_area_factory ): monitor_factory(user=staff_user) + event_area_1 = event_area_factory.create(geom=create_area_geom(), domain=monitoring_api_client.monitor.domain) + event_area_2 = event_area_factory.create(geom=create_area_geom(geom=GEOM_2), domain=staff_user.monitor.domain) + + location_1 = Point(PARKING_DATA["location"]["longitude"], PARKING_DATA["location"]["latitude"], srid=WGS84_SRID) + location_2 = Point(PARKING_DATA_2["location"]["longitude"], PARKING_DATA_2["location"]["latitude"], srid=WGS84_SRID) - parking_1 = event_parking_factory(domain=monitoring_api_client.monitor.domain) - parking_2 = event_parking_factory(domain=staff_user.monitor.domain) + parking_1 = event_parking_factory(domain=monitoring_api_client.monitor.domain, + event_area=event_area_1, location=location_1) + parking_2 = event_parking_factory(domain=staff_user.monitor.domain, event_area=event_area_2, location=location_2) result_1 = monitoring_api_client.get(list_url, data={'time': iso8601(timezone.now())}) result_2 = staff_api_client.get(list_url, data={'time': iso8601(timezone.now())}) diff --git a/run-pytest b/run-pytest index 3bdd399a..e9096d51 100755 --- a/run-pytest +++ b/run-pytest @@ -41,7 +41,6 @@ misc_opts=( "-o" "cache_dir=/tmp/pytest_cache" ) -# docker compose down docker_compose_exec=("docker compose" "exec") if ! tty -s; then