diff --git a/app/api/attendees.py b/app/api/attendees.py index c2d5505583..1e100b7ff5 100644 --- a/app/api/attendees.py +++ b/app/api/attendees.py @@ -2,10 +2,10 @@ from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship -from flask_rest_jsonapi.schema import get_relationships from sqlalchemy import and_, or_ from app.api.bootstrap import api +from app.api.helpers.custom_forms import validate_custom_form_constraints_request from app.api.helpers.db import safe_query, safe_query_kwargs from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError from app.api.helpers.permission_manager import has_access @@ -14,7 +14,6 @@ from app.api.helpers.utilities import require_relationship from app.api.schema.attendees import AttendeeSchema from app.models import db -from app.models.custom_form import CustomForms from app.models.order import Order from app.models.ticket import Ticket from app.models.ticket_holder import TicketHolder @@ -166,27 +165,6 @@ def query(self, view_kwargs): } -def validate_custom_form_constraints(self, obj, data): - relationship_fields = get_relationships(self.resource.schema) - for key, value in data.items(): - if hasattr(obj, key) and key not in relationship_fields: - setattr(obj, key, value) - - required_form_fields = CustomForms.query.filter_by( - event_id=obj.event_id, is_included=True, is_required=True - ) - missing_required_fields = [] - for field in required_form_fields.all(): - if not getattr(obj, field.identifier): - missing_required_fields.append(field.identifier) - - if len(missing_required_fields) > 0: - raise UnprocessableEntityError( - {'pointer': '/data/attributes'}, - f'Missing required fields {missing_required_fields}', - ) - - class AttendeeDetail(ResourceDetail): """ Attendee detail by id @@ -297,7 +275,9 @@ def before_update_object(self, obj, data, kwargs): obj.attendee_notes, data['attendee_notes'] ) - validate_custom_form_constraints(self, obj, data) + validate_custom_form_constraints_request( + 'attendee', self.resource.schema, obj, data + ) decorators = (jwt_required,) schema = AttendeeSchema diff --git a/app/api/helpers/custom_forms.py b/app/api/helpers/custom_forms.py new file mode 100644 index 0000000000..d6b01de016 --- /dev/null +++ b/app/api/helpers/custom_forms.py @@ -0,0 +1,40 @@ +from flask_rest_jsonapi.schema import get_relationships +from sqlalchemy import inspect + +from app.api.helpers.errors import UnprocessableEntityError +from app.models.custom_form import CustomForms + + +def object_as_dict(obj): + return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs} + + +def validate_custom_form_constraints(form, obj): + required_form_fields = CustomForms.query.filter_by( + form=form, event_id=obj.event_id, is_included=True, is_required=True + ) + missing_required_fields = [] + for field in required_form_fields.all(): + if not field.is_complex: + if not getattr(obj, field.identifier): + missing_required_fields.append(field.identifier) + else: + + if not (obj.complex_field_values or {}).get(field.identifier): + missing_required_fields.append(field.identifier) + + if len(missing_required_fields) > 0: + raise UnprocessableEntityError( + {'pointer': '/data/attributes'}, + f'Missing required fields {missing_required_fields}', + ) + + +def validate_custom_form_constraints_request(form, schema, obj, data): + new_obj = type(obj)(**object_as_dict(obj)) + relationship_fields = get_relationships(schema) + for key, value in data.items(): + if hasattr(new_obj, key) and key not in relationship_fields: + setattr(new_obj, key, value) + + validate_custom_form_constraints(form, new_obj) diff --git a/app/models/__init__.py b/app/models/__init__.py index 861d41dd62..7c3777a9ca 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -13,5 +13,6 @@ # after each test if 'pytest' in sys.modules: from objproxies import CallbackProxy + db._session = db.session db.session = CallbackProxy(lambda: db._session) diff --git a/tests/all/integration/api/attendee/test_attendee_api.py b/tests/all/integration/api/attendee/test_attendee_api.py index e97161749b..d5e9afe4d9 100644 --- a/tests/all/integration/api/attendee/test_attendee_api.py +++ b/tests/all/integration/api/attendee/test_attendee_api.py @@ -41,7 +41,7 @@ def test_edit_attendee_minimum_fields(db, client, jwt): assert attendee.lastname == 'Jamal' -def test_edit_attendee_required_fields(db, client, jwt): +def get_simple_custom_form_attendee(db): attendee = get_minimal_attendee(db) CustomForms( event=attendee.event, @@ -61,12 +61,22 @@ def test_edit_attendee_required_fields(db, client, jwt): ) db.session.commit() + return attendee + + +def test_edit_attendee_required_fields_missing(db, client, jwt): + attendee = get_simple_custom_form_attendee(db) + data = json.dumps( { 'data': { 'type': 'attendee', 'id': str(attendee.id), - "attributes": {"firstname": "Areeb", "lastname": "Jamal"}, + "attributes": { + "firstname": "Areeb", + "lastname": "Jamal", + "city": "hello@world.com", + }, } } ) @@ -93,10 +103,14 @@ def test_edit_attendee_required_fields(db, client, jwt): 'jsonapi': {'version': '1.0'}, } - assert attendee.firstname == 'Areeb' - assert attendee.lastname == 'Jamal' + assert attendee.firstname != 'Areeb' + assert attendee.lastname != 'Jamal' assert attendee.email is None + +def test_edit_attendee_required_fields_complete(db, client, jwt): + attendee = get_simple_custom_form_attendee(db) + data = json.dumps( { 'data': { @@ -129,6 +143,151 @@ def test_edit_attendee_required_fields(db, client, jwt): assert attendee.tax_business_info == 'Hello' +def get_complex_custom_form_attendee(db): + attendee = get_minimal_attendee(db) + CustomForms( + event=attendee.event, + form='attendee', + field_identifier='jobTitle', + type='text', + is_included=True, + is_required=True, + ) + CustomForms( + event=attendee.event, + form='attendee', + field_identifier='bestFriend', + name='Best Friend', + type='text', + is_included=True, + is_required=True, + is_complex=True, + ) + db.session.commit() + + return attendee + + +def test_custom_form_complex_fields_missing_required(db, client, jwt): + attendee = get_complex_custom_form_attendee(db) + + data = json.dumps( + { + 'data': { + 'type': 'attendee', + 'id': str(attendee.id), + "attributes": {"firstname": "Areeb", "lastname": "Jamal"}, + } + } + ) + + response = client.patch( + f'/v1/attendees/{attendee.id}', + content_type='application/vnd.api+json', + headers=jwt, + data=data, + ) + + db.session.refresh(attendee) + + assert response.status_code == 422 + assert json.loads(response.data) == { + 'errors': [ + { + 'detail': "Missing required fields ['best_friend', 'job_title']", + 'source': {'pointer': '/data/attributes'}, + 'status': 422, + 'title': 'Unprocessable Entity', + } + ], + 'jsonapi': {'version': '1.0'}, + } + + assert attendee.firstname != 'Areeb' + assert attendee.lastname != 'Jamal' + assert attendee.complex_field_values is None + + +def test_custom_form_complex_fields_missing_required_one(db, client, jwt): + attendee = get_complex_custom_form_attendee(db) + + data = json.dumps( + { + 'data': { + 'type': 'attendee', + 'id': str(attendee.id), + "attributes": { + "firstname": "Areeb", + "lastname": "Jamal", + "job_title": "Software Engineer", + "complex-field-values": {"favourite-friend": "Tester"}, + }, + } + } + ) + + response = client.patch( + f'/v1/attendees/{attendee.id}', + content_type='application/vnd.api+json', + headers=jwt, + data=data, + ) + + db.session.refresh(attendee) + + assert response.status_code == 422 + assert json.loads(response.data) == { + 'errors': [ + { + 'detail': "Missing required fields ['best_friend']", + 'source': {'pointer': '/data/attributes'}, + 'status': 422, + 'title': 'Unprocessable Entity', + } + ], + 'jsonapi': {'version': '1.0'}, + } + + assert attendee.firstname != 'Areeb' + assert attendee.lastname != 'Jamal' + assert attendee.complex_field_values is None + + +def test_custom_form_complex_fields_complete(db, client, jwt): + attendee = get_complex_custom_form_attendee(db) + + data = json.dumps( + { + 'data': { + 'type': 'attendee', + 'id': str(attendee.id), + "attributes": { + "firstname": "Areeb", + "lastname": "Jamal", + "job_title": "Software Engineer", + "complex-field-values": {"best_friend": "Tester"}, + }, + } + } + ) + + response = client.patch( + f'/v1/attendees/{attendee.id}', + content_type='application/vnd.api+json', + headers=jwt, + data=data, + ) + + db.session.refresh(attendee) + + assert response.status_code == 200 + + assert attendee.firstname == 'Areeb' + assert attendee.lastname == 'Jamal' + assert attendee.job_title == 'Software Engineer' + assert attendee.complex_field_values['best_friend'] == 'Tester' + + def test_edit_attendee_ticket(db, client, jwt): attendee = AttendeeOrderTicketSubFactory() ticket = TicketSubFactory(event=attendee.event)