From 86d01600229acededff468c3347d6f19f6d0d77d Mon Sep 17 00:00:00 2001 From: Tuomas Suutari Date: Fri, 15 Nov 2019 07:34:43 +0200 Subject: [PATCH 01/10] docs/operator: Specify parking disc parking API --- docs/api/operator.yaml | 134 +++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/docs/api/operator.yaml b/docs/api/operator.yaml index 0c3b0bb1..5d00c1ee 100644 --- a/docs/api/operator.yaml +++ b/docs/api/operator.yaml @@ -32,43 +32,95 @@ paths: content: application/json: schema: - type: object - example: - location: - type: Point - coordinates: [24.938466, 60.170014] - registration_number: LOL-007 - time_start: "2016-12-24T21:00:00Z" - time_end: "2016-12-24T22:00:00Z" - zone: 2 - properties: - location: - $ref: '#/components/schemas/Location' - terminal_number: - description: >- - Payment terminal number, if the parking was bought - from a payment terminal. - type: string - default: '' - registration_number: - description: Registration number for the parking - type: string - time_start: - description: Start time for the parking - type: string - format: dateTime - time_end: - description: End time for the parking - type: string - format: dateTime - zone: - description: Payment zone - type: integer - enum: [1, 2, 3] - required: - - registration_number - - time_start - - zone + anyOf: + - title: Paid parking + example: + location: + type: Point + coordinates: [24.938466, 60.170014] + registration_number: LOL-007 + time_start: "2016-12-24T21:00:00Z" + time_end: "2016-12-24T22:00:00Z" + zone: 2 + type: object + properties: + location: + $ref: '#/components/schemas/Location' + terminal_number: + description: >- + Payment terminal number, if the parking was bought + from a payment terminal. + type: string + default: '' + registration_number: + description: Registration number for the parking + type: string + time_start: + description: Start time for the parking + type: string + format: dateTime + time_end: + description: End time for the parking + type: string + format: dateTime + zone: + description: Payment zone + type: integer + enum: [1, 2, 3] + is_disc_parking: + description: >- + Specify whether this is a parking disc parking. + + Note: This field can be left out from the + request and will then default to false. This + way the API for regular paid parkings is + compatible with the previous version. + type: boolean + enum: [false] + default: false + required: + - registration_number + - time_start + - zone + - title: Parking disc parking + example: + location: + type: Point + coordinates: [24.938466, 60.170014] + registration_number: LOL-007 + time_start: "2016-12-24T21:00:00Z" + is_disc_parking: true + type: object + properties: + location: + $ref: '#/components/schemas/Location' + terminal_number: + description: >- + Payment terminal number, if the parking was bought + from a payment terminal. + type: string + default: '' + registration_number: + description: Registration number for the parking + type: string + time_start: + description: Start time for the parking + type: string + format: dateTime + time_end: + description: End time for the parking + type: string + format: dateTime + is_disc_parking: + description: >- + Specify whether this is a parking disc parking. + type: boolean + enum: [true] + required: + - registration_number + - time_start + - location + - is_disc_parking responses: '201': description: The parking was created successfully @@ -288,6 +340,14 @@ components: description: Payment zone type: integer enum: [1, 2, 3] + is_disc_parking: + description: >- + Specify whether this is a parking disc parking. + + Note: For compatibility reasons this field is present in the + result only for parking disc parkings, i.e. when the value + is true. + type: boolean required: - registration_number - time_start From 595ff56769fe4f939f12c14b5accc063838b6ffa Mon Sep 17 00:00:00 2001 From: Tuomas Suutari Date: Fri, 15 Nov 2019 10:36:19 +0200 Subject: [PATCH 02/10] docs/operator: Specify Parking Info Queries --- docs/api/operator.yaml | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/docs/api/operator.yaml b/docs/api/operator.yaml index 5d00c1ee..404cb16e 100644 --- a/docs/api/operator.yaml +++ b/docs/api/operator.yaml @@ -19,6 +19,9 @@ tags: - name: Parkings description: >- Endpoints for creating and updating parkings + - name: Parking Information Queries + description: >- + Endpoint for performing parking spot information queries paths: /parking/: post: @@ -275,6 +278,53 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + /parking_info_query/: + post: + tags: ['Parking Information Queries'] + summary: Query parking spot information + operationId: queryParkingInfo + security: [{ApiKey: []}] + requestBody: + required: true + description: Location of the parking spot to query + content: + application/json: + schema: + anyOf: + - title: By GPS coordinates + example: + location: + type: Point + coordinates: [24.938466, 60.170014] + type: object + properties: + location: + $ref: '#/components/schemas/Location' + required: + - location + - title: By payment terminal number + example: + terminal_number: "34B" + type: object + properties: + terminal_number: + description: Payment terminal number + type: string + required: + - terminal_number + responses: + '200': + description: Parking spot information query succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/ParkingInfo' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' components: securitySchemes: ApiKey: @@ -373,6 +423,91 @@ components: items: type: number format: float + ParkingInfo: + description: Parking spot information + type: object + example: + location: + type: "Point" + coordinates: [24.938466, 60.170014] + terminal_number: "34B" + zone: 2 + rules: + - after: "2019-11-15T06:00:00Z" + policy: "disc" + maximum_duration: 120 + - after: "2019-11-15T16:00:00Z" + policy: "free" + - after: "2019-11-16T07:00:00Z" + policy: "disc" + maximum_duration: 120 + - after: "2019-11-16T13:00:00Z" + policy: "free" + - after: "2019-11-17T02:00:00Z" + policy: "denied" + - after: "2019-11-18T02:00:00Z" + policy: "free" + - after: "2019-11-18T06:00:00Z" + policy: "disc" + maximum_duration: 120 + - after: "2019-11-18T16:00:00Z" + policy: "free" + - after: "2019-11-19T02:00:00Z" + policy: "unknown" + required: + - location + - zone + - rules + properties: + location: + $ref: '#/components/schemas/Location' + terminal_number: + description: >- + Number of the closest payment terminal, if there is any near + the given location. + type: string + zone: + description: Payment zone of the parking spot + type: integer + rules: + description: >- + List of rules that determine if parking is allowed and on + what conditions. Each rule is valid for a time period + starting from the "after" timestamp specified in the rule + and ending to "after" timestamp of the next rule. + type: array + maxItems: 100 + items: + type: object + required: + - after + - policy + properties: + after: + description: Start time of this rule + type: string + format: dateTime + policy: + description: >- + What kind of parking is allowed during this period. + + Options are: + * `paid`: Must pay the parking fee + * `disc`: Must use a parking disc (regular or digital) + * `free`: Parking is allowed for free without a disc + * `denied`: Parking is not allowed + * `unknown`: There is no information available + type: string + enum: ["paid", "disc", "free", "denied", "unknown"] + maximum_duration: + description: >- + Maximum duration of parking during this period, in + minutes. + + Note: Start times of parking disc parkings are rounded + to next half hour, which might allow almost 30 minutes + more parking time in practice. + type: integer responses: BadRequest: # 400 description: Bad request, details in request body From 4f10e836c613974de1f372b92991c32a6114f35c Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Tue, 19 Nov 2019 13:29:13 +0200 Subject: [PATCH 03/10] Add disc parking changes to Parking model * Add is_disc_parking field to specify whether the parking is disc parking or normal parking. * Make zone not required field since disc parking no longer needs zone information. --- .../migrations/0028_digital_disc_changes.py | 31 +++++++++++++++++++ parkings/models/parking.py | 9 ++++-- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 parkings/migrations/0028_digital_disc_changes.py diff --git a/parkings/migrations/0028_digital_disc_changes.py b/parkings/migrations/0028_digital_disc_changes.py new file mode 100644 index 00000000..c96e1eaa --- /dev/null +++ b/parkings/migrations/0028_digital_disc_changes.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.3 on 2019-11-14 14:23 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parkings', '0027_parkingcheck_performer'), + ] + + operations = [ + migrations.AddField( + model_name='parking', + name='is_disc_parking', + field=models.BooleanField(default=False, verbose_name='disc parking'), + ), + migrations.AlterField( + model_name='parking', + name='zone', + field=models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(3) + ], + verbose_name='zone number'), + ), + ] diff --git a/parkings/models/parking.py b/parkings/models/parking.py index 6eacb892..ac97c9c4 100644 --- a/parkings/models/parking.py +++ b/parkings/models/parking.py @@ -77,9 +77,12 @@ class Parking(TimestampedModelMixin, UUIDPrimaryKeyMixin): time_end = models.DateTimeField( verbose_name=_("parking end time"), db_index=True, null=True, blank=True, ) - zone = models.IntegerField(verbose_name=_("zone number"), validators=[ - MinValueValidator(1), MaxValueValidator(3), - ]) + zone = models.IntegerField( + verbose_name=_("zone number"), + null=True, blank=True, + validators=[MinValueValidator(1), MaxValueValidator(3), ] + ) + is_disc_parking = models.BooleanField(verbose_name=_("disc parking"), default=False) objects = ParkingQuerySet.as_manager() From d7b4e8fa0037593cf3f5aedadaa215eacf77b1a4 Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Tue, 19 Nov 2019 13:55:02 +0200 Subject: [PATCH 04/10] Change the required fields based on the type of parking registration_number and time_start are mandatory fields for both parking. In additon to these location is required for disc parking while zone is required for normal parking. Therefore, set the required fields accordingly in serializer. --- parkings/api/operator/parking.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/parkings/api/operator/parking.py b/parkings/api/operator/parking.py index c6688ebf..c2ffe3cb 100644 --- a/parkings/api/operator/parking.py +++ b/parkings/api/operator/parking.py @@ -20,7 +20,7 @@ class Meta: 'registration_number', 'time_start', 'time_end', 'zone', - 'status', + 'status', 'is_disc_parking', ) # these are needed because by default a PUT request that does not contain some optional field @@ -30,12 +30,22 @@ class Meta: 'location': {'default': None}, 'terminal_number': {'default': ''}, 'time_end': {'default': None}, + 'is_disc_parking': {'default': False}, } def __init__(self, *args, **kwargs): super(OperatorAPIParkingSerializer, self).__init__(*args, **kwargs) self.fields['time_start'].timezone = pytz.utc self.fields['time_end'].timezone = pytz.utc + self.fields['zone'].required = True + + self._set_required_extra_fields() + + def _set_required_extra_fields(self): + initial_data = getattr(self, 'initial_data', None) + if initial_data and self.initial_data.get('is_disc_parking', False): + self.fields['location'].required = True + self.fields['zone'].required = False def validate(self, data): if self.instance and (now() - self.instance.created_at) > settings.PARKKIHUBI_TIME_PARKINGS_EDITABLE: From bb1e7c840d527d78d0ed6ca6a6c443287374fff7 Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Tue, 19 Nov 2019 13:58:55 +0200 Subject: [PATCH 05/10] Remove the is_disc_parking field for normal parking's response Remove the is_disc_parking field from the response for the normal parkings so that it won't break any existing implementation. Only return this field on responses for disc parking. --- parkings/api/operator/parking.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/parkings/api/operator/parking.py b/parkings/api/operator/parking.py index c2ffe3cb..6bc4ef5d 100644 --- a/parkings/api/operator/parking.py +++ b/parkings/api/operator/parking.py @@ -68,6 +68,12 @@ def validate(self, data): return data + def to_representation(self, instance): + representation = super().to_representation(instance) + if not instance.is_disc_parking: + representation.pop('is_disc_parking') + return representation + class OperatorAPIParkingPermission(permissions.BasePermission): def has_permission(self, request, view): From e06aa9b93e73a9c146a5254f1009991bb11ff803 Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Tue, 19 Nov 2019 14:01:28 +0200 Subject: [PATCH 06/10] Add tests for the disc parking --- parkings/factories/__init__.py | 3 +- parkings/factories/parking.py | 4 ++ .../tests/api/operator/test_disc_parking.py | 68 +++++++++++++++++++ parkings/tests/api/operator/test_parking.py | 2 + parkings/tests/conftest.py | 8 ++- 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 parkings/tests/api/operator/test_disc_parking.py diff --git a/parkings/factories/__init__.py b/parkings/factories/__init__.py index 611433c5..bb6b9873 100644 --- a/parkings/factories/__init__.py +++ b/parkings/factories/__init__.py @@ -1,5 +1,5 @@ from .operator import OperatorFactory # noqa -from .parking import HistoryParkingFactory, ParkingFactory # noqa +from .parking import DiscParkingFactory, HistoryParkingFactory, ParkingFactory # noqa from .parking_area import ParkingAreaFactory # noqa from .permit import ActivePermitFactory, PermitFactory, PermitSeriesFactory from .region import RegionFactory @@ -8,6 +8,7 @@ __all__ = [ 'ActivePermitFactory', 'AdminUserFactory', + 'DiscParkingFactory', 'HistoryParkingFactory', 'OperatorFactory', 'ParkingAreaFactory', diff --git a/parkings/factories/parking.py b/parkings/factories/parking.py index 40c1e11d..8ad016aa 100644 --- a/parkings/factories/parking.py +++ b/parkings/factories/parking.py @@ -32,6 +32,10 @@ class Meta: zone = factory.LazyFunction(lambda: fake.random.randint(1, 3)) +class DiscParkingFactory(ParkingFactory): + is_disc_parking = True + + def get_time_far_enough_in_past(): return fake.date_time_this_decade(before_now=True, tzinfo=pytz.utc) - timedelta(days=7, seconds=1) diff --git a/parkings/tests/api/operator/test_disc_parking.py b/parkings/tests/api/operator/test_disc_parking.py new file mode 100644 index 00000000..49ec0e03 --- /dev/null +++ b/parkings/tests/api/operator/test_disc_parking.py @@ -0,0 +1,68 @@ +import datetime + +import pytest +from django.urls import reverse + +from parkings.models import Parking + +from ..utils import delete, patch, post + +list_url = reverse('operator:v1:parking-list') + + +def get_detail_url(obj): + return reverse('operator:v1:parking-detail', kwargs={'pk': obj.pk}) + + +@pytest.fixture +def disc_parking_data(): + return { + 'registration_number': 'VSM-162', + 'time_start': '2016-12-12T20:34:38Z', + 'location': {'coordinates': [60.16899227603715, 24.9482582558314], 'type': 'Point'}, + 'is_disc_parking': True + } + + +expected_keys = { + 'id', 'zone', 'registration_number', + 'terminal_number', + 'time_start', 'time_end', + 'location', 'created_at', 'modified_at', + 'status', 'is_disc_parking', + } + + +def test_post_disc_parking(operator_api_client, operator, disc_parking_data): + response_parking_data = post(operator_api_client, list_url, disc_parking_data) + + returned_data_keys = set(response_parking_data) + posted_data_keys = set(disc_parking_data) + assert returned_data_keys == expected_keys + + for key in returned_data_keys & posted_data_keys: + assert response_parking_data[key] == disc_parking_data[key] + + +def test_end_disc_parking_with_patch(operator_api_client, operator, disc_parking): + detail_url = get_detail_url(disc_parking) + + new_time_end = disc_parking.time_end + datetime.timedelta(days=1) + time_end = new_time_end.strftime('%Y-%m-%dT%H:%M:%SZ') + + patch(operator_api_client, detail_url, data={'time_end': time_end}) + disc_parking.refresh_from_db() + + assert disc_parking.time_end.strftime('%Y-%m-%dT%H:%M:%SZ') == time_end + + +def test_delete_disc_parking(operator_api_client, disc_parking): + detail_url = get_detail_url(disc_parking) + delete(operator_api_client, detail_url) + + assert not Parking.objects.filter(id=disc_parking.id).exists() + + +def test_required_fields_for_disc_parking(operator_api_client, operator, disc_parking_data): + disc_parking_data.pop('location') + post(operator_api_client, list_url, disc_parking_data, status_code=400) diff --git a/parkings/tests/api/operator/test_parking.py b/parkings/tests/api/operator/test_parking.py index b7a9ced1..23e59f97 100644 --- a/parkings/tests/api/operator/test_parking.py +++ b/parkings/tests/api/operator/test_parking.py @@ -78,6 +78,8 @@ def check_response_parking_data(posted_parking_data, response_parking_data): for key in returned_data_keys & posted_data_keys: assert response_parking_data[key] == posted_parking_data[key] + assert 'is_disc_parking' not in set(response_parking_data) + def test_disallowed_methods(operator_api_client, parking): list_disallowed_methods = ('get', 'put', 'patch', 'delete') diff --git a/parkings/tests/conftest.py b/parkings/tests/conftest.py index 50b82772..f525ac62 100644 --- a/parkings/tests/conftest.py +++ b/parkings/tests/conftest.py @@ -2,9 +2,10 @@ from pytest_factoryboy import register from parkings.factories import ( - ActivePermitFactory, AdminUserFactory, HistoryParkingFactory, - OperatorFactory, ParkingAreaFactory, ParkingFactory, PermitFactory, - PermitSeriesFactory, RegionFactory, StaffUserFactory, UserFactory) + ActivePermitFactory, AdminUserFactory, DiscParkingFactory, + HistoryParkingFactory, OperatorFactory, ParkingAreaFactory, ParkingFactory, + PermitFactory, PermitSeriesFactory, RegionFactory, StaffUserFactory, + UserFactory) register(OperatorFactory) register(ParkingFactory, 'parking') @@ -17,6 +18,7 @@ register(PermitFactory, 'permit') register(PermitSeriesFactory, 'permit_series') register(ActivePermitFactory, 'active_permit') +register(DiscParkingFactory, 'disc_parking') @pytest.fixture(autouse=True) From f7c5ef1847126cddc8430bffdbe291b72ad2ceb5 Mon Sep 17 00:00:00 2001 From: Tuomas Suutari Date: Fri, 10 Jan 2020 15:08:06 +0200 Subject: [PATCH 07/10] docs/enforcement: Add forgotten Parking.operator field This field has actually been there already since commit 4716c3b923. --- docs/api/enforcement.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api/enforcement.yaml b/docs/api/enforcement.yaml index ba987ee8..558ac980 100644 --- a/docs/api/enforcement.yaml +++ b/docs/api/enforcement.yaml @@ -657,6 +657,10 @@ components: zone: description: Parking zone id type: integer + operator: + description: Id of the operator + type: string + format: uuid operator_name: description: Name of the operator type: string From 022a1ec0a816b7d0e462953c110098bf5f3bbc83 Mon Sep 17 00:00:00 2001 From: Tuomas Suutari Date: Fri, 10 Jan 2020 15:15:02 +0200 Subject: [PATCH 08/10] docs/enforcement: Add is_disc_parking field Add is_disc_parking field to the Parking object specification which is used by the valid_parking endpoint. --- docs/api/enforcement.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api/enforcement.yaml b/docs/api/enforcement.yaml index 558ac980..d6b8d6ca 100644 --- a/docs/api/enforcement.yaml +++ b/docs/api/enforcement.yaml @@ -657,6 +657,14 @@ components: zone: description: Parking zone id type: integer + is_disc_parking: + description: >- + Specify whether this is a parking disc parking. + + Note: For compatibility reasons this field is present in the + result only for parking disc parkings, i.e. when the value + is true. + type: boolean operator: description: Id of the operator type: string From 0eed324f93d8c7a2e6d51567a9086fdc0a38fe9c Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Tue, 14 Jan 2020 14:30:48 +0200 Subject: [PATCH 09/10] Add disc parking info to enforcement parking validity Add is_disc_parking field to response of enforcement parking validity if the parking instance is disc parking. Remove this field for the normal parking's response. --- parkings/api/enforcement/valid_parking.py | 9 +++++++ .../api/enforcement/test_valid_parking.py | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/parkings/api/enforcement/valid_parking.py b/parkings/api/enforcement/valid_parking.py index 440f1491..fd6e86d2 100644 --- a/parkings/api/enforcement/valid_parking.py +++ b/parkings/api/enforcement/valid_parking.py @@ -24,8 +24,17 @@ class Meta: 'zone', 'operator', 'operator_name', + 'is_disc_parking', ] + def to_representation(self, instance): + representation = super().to_representation(instance) + + if not instance.is_disc_parking: + representation.pop('is_disc_parking') + + return representation + class ValidParkingFilter(django_filters.rest_framework.FilterSet): reg_num = django_filters.CharFilter( diff --git a/parkings/tests/api/enforcement/test_valid_parking.py b/parkings/tests/api/enforcement/test_valid_parking.py index 56a134e6..c2deeb4c 100644 --- a/parkings/tests/api/enforcement/test_valid_parking.py +++ b/parkings/tests/api/enforcement/test_valid_parking.py @@ -1,6 +1,7 @@ from datetime import datetime import pytest +from django.conf import settings from django.urls import reverse from django.utils.timezone import utc from rest_framework.status import ( @@ -31,7 +32,16 @@ def get_url(kind, parking): kwargs={'pk': parking.pk}) +def get_parking_object(parking_type, parking, disc_parking): + if parking_type == 'paid': + return parking + else: + assert parking_type == 'disc_parking' + return disc_parking + + ALL_URL_KINDS = ['list', 'list_by_reg_num', 'detail'] +ALL_PARKING_KINDS = ['paid', 'disc_parking'] @pytest.mark.parametrize('url_kind', ALL_URL_KINDS) @@ -63,22 +73,29 @@ def test_list_endpoint_base_fields(staff_api_client): check_list_endpoint_base_fields(parking_data) -def test_list_endpoint_data(staff_api_client, parking): - data = get(staff_api_client, get_url('list_by_reg_num', parking)) +@pytest.mark.parametrize('parking_type', ALL_PARKING_KINDS) +def test_list_endpoint_data(staff_api_client, parking_type, parking, disc_parking): + parking_object = get_parking_object(parking_type, parking, disc_parking) + data = get(staff_api_client, get_url('list_by_reg_num', parking_object)) assert len(data['results']) == 1 parking_data = data['results'][0] check_parking_data_keys(parking_data) - check_parking_data_matches_parking_object(data['results'][0], parking) + check_parking_data_matches_parking_object(data['results'][0], parking_object) def check_parking_data_keys(parking_data): - assert set(parking_data.keys()) == { + parking_data_keys = { 'id', 'created_at', 'modified_at', 'registration_number', 'time_start', 'time_end', 'zone', 'operator', 'operator_name', } + if parking_data.get('is_disc_parking'): + parking_data_keys.add('is_disc_parking') + + assert set(parking_data.keys()) == parking_data_keys + def check_parking_data_matches_parking_object(parking_data, parking_obj): """ @@ -96,6 +113,7 @@ def check_parking_data_matches_parking_object(parking_data, parking_obj): assert parking_data['time_start'] == iso8601(parking_obj.time_start) assert parking_data['time_end'] == iso8601(parking_obj.time_end) assert parking_data['operator_name'] == str(parking_obj.operator.name) + assert parking_data.get('is_disc_parking', False) == parking_obj.is_disc_parking def iso8601(dt): From 0d389f3ce4354639f62772cd61796b6db0886568 Mon Sep 17 00:00:00 2001 From: Deepak Panta Date: Wed, 15 Jan 2020 17:02:20 +0200 Subject: [PATCH 10/10] Make it possible to override empty end times in valid parkings PASI system had the problem when the time_end field was null in response. Fix this issue by having hard coded date-time string if the time_end is empty. --- parkings/api/enforcement/valid_parking.py | 5 +++++ .../tests/api/enforcement/test_valid_parking.py | 15 ++++++++++++++- parkkihubi/settings.py | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/parkings/api/enforcement/valid_parking.py b/parkings/api/enforcement/valid_parking.py index fd6e86d2..3f48c7e8 100644 --- a/parkings/api/enforcement/valid_parking.py +++ b/parkings/api/enforcement/valid_parking.py @@ -33,6 +33,11 @@ def to_representation(self, instance): if not instance.is_disc_parking: representation.pop('is_disc_parking') + if instance.time_end is None: + replacement_value = getattr( + settings, 'PARKKIHUBI_NONE_END_TIME_REPLACEMENT', None) + representation['time_end'] = replacement_value or None + return representation diff --git a/parkings/tests/api/enforcement/test_valid_parking.py b/parkings/tests/api/enforcement/test_valid_parking.py index c2deeb4c..1d320248 100644 --- a/parkings/tests/api/enforcement/test_valid_parking.py +++ b/parkings/tests/api/enforcement/test_valid_parking.py @@ -1,7 +1,7 @@ from datetime import datetime import pytest -from django.conf import settings +from django.test import override_settings from django.urls import reverse from django.utils.timezone import utc from rest_framework.status import ( @@ -190,3 +190,16 @@ def test_time_filtering(operator, staff_api_client, parking_factory, name): filtering = '&time={}'.format(time) if time else '' response = get(staff_api_client, list_url_for_abc + filtering) check_response_objects(response, expected_parkings) + + +@pytest.mark.parametrize('parking_type', ALL_PARKING_KINDS) +@override_settings(PARKKIHUBI_NONE_END_TIME_REPLACEMENT='2030-12-31T23:59:59Z') +def test_null_time_end_is_replaced_correctly(parking_type, staff_api_client, parking, disc_parking): + parking_object = get_parking_object(parking_type, parking, disc_parking) + parking_object.time_end = None + parking_object.save() + data = get(staff_api_client, get_url('list_by_reg_num', parking_object)) + parking_data = data['results'][0] + check_parking_data_keys(parking_data) + + assert parking_data['time_end'] == '2030-12-31T23:59:59Z' diff --git a/parkkihubi/settings.py b/parkkihubi/settings.py index 4a39f88b..5d10142f 100644 --- a/parkkihubi/settings.py +++ b/parkkihubi/settings.py @@ -207,6 +207,8 @@ MONITORING_GROUP_NAME = 'monitoring' PARKKIHUBI_TIME_PARKINGS_EDITABLE = timedelta(minutes=2) PARKKIHUBI_TIME_OLD_PARKINGS_VISIBLE = timedelta(minutes=15) +PARKKIHUBI_NONE_END_TIME_REPLACEMENT = env.str( + 'PARKKIHUBI_NONE_END_TIME_REPLACEMENT', default='') PARKKIHUBI_PUBLIC_API_ENABLED = env.bool('PARKKIHUBI_PUBLIC_API_ENABLED', True) PARKKIHUBI_MONITORING_API_ENABLED = env.bool( 'PARKKIHUBI_MONITORING_API_ENABLED', True)