diff --git a/resources/api/base.py b/resources/api/base.py index 951fa6dfe..8f5300631 100644 --- a/resources/api/base.py +++ b/resources/api/base.py @@ -101,6 +101,12 @@ def validate_translation(self, data): fields = [(key, data[key]) for key in data if key in self.translated_fields] for field, value in fields: for lang in [x[0] for x in settings.LANGUAGES]: + if value is None and self.fields[field].allow_null: + data.update({ + '%s_%s' % (field, lang): None + }) + continue + if (not lang in value or not value[lang]) and '%s_%s' % (field, lang) in self.Meta.required_translations: raise ValidationError({ field: [ @@ -119,10 +125,10 @@ def validate_translation(self, data): return data def validate(self, attrs): - attrs = super().validate(attrs) - if getattr(self.Meta, 'required_translations', None): - self.validate_translation(attrs) - return attrs + attrs = super().validate(attrs) + if getattr(self.Meta, 'required_translations', None): + self.validate_translation(attrs) + return attrs class NullableTimeField(serializers.TimeField): diff --git a/resources/api/resource.py b/resources/api/resource.py index d95e6ebba..423646be6 100644 --- a/resources/api/resource.py +++ b/resources/api/resource.py @@ -1583,11 +1583,13 @@ class ResourceCreateSerializer(TranslatedModelSerializer): description = serializers.DictField(required=True) responsible_contact_info = serializers.DictField( required=False, - help_text=get_translated_field_help_text('responsible_contact_info') + help_text=get_translated_field_help_text('responsible_contact_info'), + allow_null=True ) specific_terms = serializers.DictField( required=False, - help_text=get_translated_field_help_text('specific_terms') + help_text=get_translated_field_help_text('specific_terms'), + allow_null=True ) need_manual_confirmation = serializers.BooleanField(required=True) authentication = serializers.ChoiceField(choices=Resource.AUTHENTICATION_TYPES, required=True) @@ -1597,9 +1599,9 @@ class ResourceCreateSerializer(TranslatedModelSerializer): slot_size = serializers.DurationField(required=True) reservation_info = serializers.DictField(required=True) - reservation_confirmed_notification_extra = serializers.DictField(required=False) - reservation_requested_notification_extra = serializers.DictField(required=False) - reservation_additional_information = serializers.DictField(required=False) + reservation_confirmed_notification_extra = serializers.DictField(required=False, allow_null=True) + reservation_requested_notification_extra = serializers.DictField(required=False, allow_null=True) + reservation_additional_information = serializers.DictField(required=False, allow_null=True) resource_staff_emails = ResourceStaffEmailsField(required=False, allow_empty=True) diff --git a/resources/models/resource.py b/resources/models/resource.py index 6619418ff..2d58a310d 100644 --- a/resources/models/resource.py +++ b/resources/models/resource.py @@ -1036,7 +1036,7 @@ def _process_image(self): save_kwargs = {} with Image.open(self.image) as img: if img.size > (1920, 1080): - img.thumbnail((1920, 1080), Image.ANTIALIAS) + img.thumbnail((1920, 1080), Image.LANCZOS) self.cropping = None setattr(self, '_processing_required', True) elif img.size < (128, 128): diff --git a/resources/tests/conftest.py b/resources/tests/conftest.py index 4f5202505..676a63909 100644 --- a/resources/tests/conftest.py +++ b/resources/tests/conftest.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pytest import datetime +import base64 from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.utils import timezone @@ -14,6 +15,7 @@ from resources.models import ReservationMetadataSet, ReservationMetadataField from munigeo.models import Municipality from maintenance.models import MaintenanceMessage, MaintenanceMode +from .utils import get_test_image_data, get_test_image_payload @pytest.fixture def api_client(): @@ -668,4 +670,42 @@ def resource_with_active_reservations(resource_in_unit): end=datetime.datetime(year=2115, month=4, day=4, hour=i+1, minute=0, second=0)) \ for i in range(1,11) ]) - return resource_in_unit \ No newline at end of file + return resource_in_unit + + +@pytest.fixture +def resource_create_data( + purpose, test_unit, + space_resource_type): + image = get_test_image_data() + return { + "public": True, + "purposes": [ + purpose.id + ], + "name": { + "fi": "Test Resource API", + "en": "Test Resource API", + "sv": "Test Resource API", + }, + "description": { + "fi": "Test Resource created through API", + "en": "Test Resource created through API", + "sv": "Test Resource created through API" + }, + "reservation_info": { + "fi": "Test Resource reservation information", + "en": "Test Resource reservation information", + "sv": "Test Resource reservation information" + }, + "need_manual_confirmation": False, + "min_period": "00:30:00", + "max_period": "01:00:00", + "slot_size": "00:15:00", + "authentication": "strong", + "people_capacity": "10", + "terms_of_use": [], + "unit": test_unit.pk, + "type": space_resource_type.pk, + "images": [get_test_image_payload(image=image)] + } diff --git a/resources/tests/test_resource_api.py b/resources/tests/test_resource_api.py index a106e92ea..538933319 100644 --- a/resources/tests/test_resource_api.py +++ b/resources/tests/test_resource_api.py @@ -1,5 +1,6 @@ import datetime import pytest +import base64 from copy import deepcopy from django.urls import reverse from django.contrib.auth import get_user_model @@ -10,13 +11,21 @@ from freezegun import freeze_time from guardian.shortcuts import assign_perm, remove_perm -from resources.models.resource import Resource +from resources.models.resource import ( + Resource, ResourceImage, InvalidImage +) from ..enums import UnitAuthorizationLevel, UnitGroupAuthorizationLevel -from resources.models import (Day, Equipment, Period, Reservation, ReservationMetadataSet, ResourceEquipment, - ResourceType, Unit, UnitAuthorization, UnitGroup) -from .utils import assert_response_objects, check_only_safe_methods_allowed, is_partial_dict_in_list, MAX_QUERIES - +from resources.models import ( + Day, Equipment, Period, Reservation, + ReservationMetadataSet, ResourceEquipment, + ResourceType, Unit, UnitGroup +) +from .utils import ( + assert_response_objects, check_only_safe_methods_allowed, + is_partial_dict_in_list, MAX_QUERIES, + get_test_image_data, get_test_image_payload +) @pytest.fixture def list_url(): @@ -1332,4 +1341,68 @@ def test_resource_mass_cancel_reservation_permitted_for_admin_user( response = staff_api_client.delete(url, data=payload, HTTP_ACCEPT_LANGUAGE='en') assert response.status_code == 204 - assert resource_with_active_reservations.reservations.current().count() == 0 \ No newline at end of file + assert resource_with_active_reservations.reservations.current().count() == 0 + + +@pytest.mark.django_db +@pytest.mark.parametrize('image_size, gets_processed', ( + ((128, 128), False), + ((1980, 1200), True), +)) +def test_resource_create_through_api( + staff_api_client, staff_user, + resource_create_data, list_url, + image_size, gets_processed +): + url = f'{list_url[:-1]}/new/' + assign_perm('resources.add_resource', staff_user) + staff_api_client.force_authenticate(user=staff_user) + image = get_test_image_data(image_size) + resource_create_data['images'] = [get_test_image_payload(image)] + response = staff_api_client.post(url, data=resource_create_data) + assert response.status_code == 201 + resource = Resource.objects.get(pk=response.data['id']) + + resource_image = resource.images.first().image + + if gets_processed: + assert (resource_image.width, resource_image.height) < (1921, 1081) + else: + assert (resource_image.width, resource_image.height) == image_size + + +@pytest.mark.django_db +def test_resource_create_through_api_invalid_image( + staff_api_client, staff_user, + resource_create_data, list_url +): + image = get_test_image_data((64, 64)) + + url = f'{list_url[:-1]}/new/' + assign_perm('resources.add_resource', staff_user) + staff_api_client.force_authenticate(user=staff_user) + resource_create_data['images'] = [get_test_image_payload(image)] + with pytest.raises(InvalidImage): + staff_api_client.post(url, data=resource_create_data) + + +@pytest.mark.django_db +def test_resource_update_optional_fields_to_null( + staff_api_client, staff_user, + detail_url +): + assign_perm('resources.change_resource', staff_user) + url = f'{detail_url[:-1]}/update/' + staff_api_client.force_authenticate(user=staff_user) + + optional_fields = { + 'responsible_contact_info': None, + 'specific_terms': None, + 'reservation_confirmed_notification_extra': None, + 'reservation_requested_notification_extra': None, + 'reservation_additional_information': None + } + + response = staff_api_client.patch(url, data=optional_fields) + + assert response.status_code == 200 diff --git a/resources/tests/utils.py b/resources/tests/utils.py index bede61185..1a629b7fd 100644 --- a/resources/tests/utils.py +++ b/resources/tests/utils.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +import base64 from django.core.exceptions import ValidationError from django.core.files.base import ContentFile @@ -197,3 +198,22 @@ def check_keys(data, expected_keys): def is_partial_dict_in_list(partial, dicts): partial_items = partial.items() return any([partial_items <= d.items() for d in dicts]) + + +def get_test_image_payload( + image, *, + type = "main", + caption = "test caption", + name = "test_image.jpg"): + return { + "type": type, + "caption": { + "fi": caption, + "sv": caption, + "en": caption + }, + "image": { + "name": name, + "data": base64.b64encode(image).decode() + } + } diff --git a/respa/__init__.py b/respa/__init__.py index b41cc61f8..bbbf75f5d 100644 --- a/respa/__init__.py +++ b/respa/__init__.py @@ -1,3 +1,3 @@ -__version__ = 'tku-v1.9.1' +__version__ = 'tku-v1.9.2' VERSION = __version__