diff --git a/messages.pot b/messages.pot index abb46e2..35f58c2 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-07-09 21:57+0200\n" +"POT-Creation-Date: 2023-07-13 15:39+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -201,7 +201,7 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:479 +#: project/api/organization/resources.py:645 #: project/views/admin_unit_member_invitation.py:89 msgid "You have received an invitation" msgstr "" @@ -2561,7 +2561,7 @@ msgstr "" #: project/views/admin.py:85 project/views/admin_unit.py:202 #: project/views/admin_unit.py:235 project/views/manage.py:315 -#: project/views/verification_request.py:167 +#: project/views/verification_request.py:168 msgid "Entered name does not match organization name" msgstr "" @@ -2650,31 +2650,31 @@ msgstr "" msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:205 +#: project/views/event.py:208 msgid "Event successfully published" msgstr "" -#: project/views/event.py:207 +#: project/views/event.py:210 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:209 +#: project/views/event.py:212 msgid "Event successfully planned" msgstr "" -#: project/views/event.py:266 +#: project/views/event.py:272 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:292 +#: project/views/event.py:298 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:457 +#: project/views/event.py:463 msgid "Referenced event changed" msgstr "" -#: project/views/event.py:480 +#: project/views/event.py:486 msgid "New event report" msgstr "" @@ -2699,7 +2699,7 @@ msgid "Event suggestion successfully rejected" msgstr "" #: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:135 +#: project/views/reference_request_review.py:129 msgid "Event review status updated" msgstr "" @@ -2779,45 +2779,44 @@ msgstr "" msgid "Reference successfully updated" msgstr "" -#: project/views/reference.py:153 +#: project/views/reference.py:156 msgid "Reference successfully deleted" msgstr "" -#: project/views/reference_request.py:138 +#: project/views/reference_request.py:145 #, python-format msgid "%(organization)s accepted your reference request" msgstr "" -#: project/views/reference_request.py:145 +#: project/views/reference_request.py:152 #, python-format msgid "" "Reference request to %(organization)s successfully created. You will be " "notified after the other organization reviews the event." msgstr "" -#: project/views/reference_request.py:168 +#: project/views/reference_request.py:172 msgid "New reference request" msgstr "" -#: project/views/reference_request.py:177 +#: project/views/reference_request.py:181 msgid "New reference automatically verified" msgstr "" #: project/views/reference_request_review.py:34 -#: project/views/verification_request_review.py:28 msgid "Request already verified" msgstr "" -#: project/views/reference_request_review.py:57 +#: project/views/reference_request_review.py:51 msgid "Reference successfully created" msgstr "" -#: project/views/reference_request_review.py:65 +#: project/views/reference_request_review.py:59 msgid "Request successfully updated" msgstr "" -#: project/views/reference_request_review.py:88 -#: project/views/verification_request_review.py:79 +#: project/views/reference_request_review.py:82 +#: project/views/verification_request_review.py:73 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " @@ -2863,29 +2862,33 @@ msgid "" " the invitation was sent to." msgstr "" -#: project/views/verification_request.py:131 +#: project/views/verification_request.py:132 msgid "" "Request successfully created. You will be notified after the other " "organization reviewed the request." msgstr "" -#: project/views/verification_request.py:172 +#: project/views/verification_request.py:173 msgid "Verification request successfully deleted" msgstr "" -#: project/views/verification_request.py:205 +#: project/views/verification_request.py:206 msgid "New verification request" msgstr "" -#: project/views/verification_request_review.py:60 +#: project/views/verification_request_review.py:28 +msgid "Verification request already verified" +msgstr "" + +#: project/views/verification_request_review.py:54 msgid "Organization successfully verified" msgstr "" -#: project/views/verification_request_review.py:62 +#: project/views/verification_request_review.py:56 msgid "Verification request successfully updated" msgstr "" -#: project/views/verification_request_review.py:115 +#: project/views/verification_request_review.py:109 msgid "Verification request review status updated" msgstr "" diff --git a/project/api/__init__.py b/project/api/__init__.py index e87d3b9..e8969aa 100644 --- a/project/api/__init__.py +++ b/project/api/__init__.py @@ -113,6 +113,9 @@ class RestApi(Api): if len(errors) > 0: data["errors"] = errors + elif isinstance(arg, str): + if arg: + data["message"] = arg class DocSecurityPlugin(BasePlugin): @@ -135,6 +138,8 @@ scope_list = [ "eventreference:write", "eventreferencerequest:read", "eventreferencerequest:write", + "organizationverificationrequest:read", + "organizationverificationrequest:write", ] scopes = {k: get_localized_scope(k) for v, k in enumerate(scope_list)} @@ -226,6 +231,7 @@ import project.api.event_reference_request.resources import project.api.organization.resources import project.api.organization_invitation.resources import project.api.organization_relation.resources +import project.api.organization_verification_request.resources import project.api.organizer.resources import project.api.place.resources import project.api.user.resources diff --git a/project/api/event_reference_request/schemas.py b/project/api/event_reference_request/schemas.py index ada812e..0cb8460 100644 --- a/project/api/event_reference_request/schemas.py +++ b/project/api/event_reference_request/schemas.py @@ -1,7 +1,6 @@ from marshmallow import fields, validate from marshmallow_enum import EnumField -from project.api import marshmallow from project.api.event.schemas import EventRefSchema, EventWriteIdSchema from project.api.organization.schemas import ( OrganizationRefSchema, @@ -55,11 +54,6 @@ class EventReferenceRequestSchema( organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit") -class EventReferenceRequestDumpSchema(EventReferenceRequestIdSchema): - event_id = marshmallow.auto_field() - organization_id = fields.Int(attribute="admin_unit_id") - - class EventReferenceRequestListRequestSchema( PaginationRequestSchema, TrackableRequestSchemaMixin ): diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 120f7c0..68ebfd6 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -70,6 +70,12 @@ from project.api.organization_relation.schemas import ( OrganizationRelationListResponseSchema, OrganizationRelationSchema, ) +from project.api.organization_verification_request.schemas import ( + OrganizationVerificationRequestIdSchema, + OrganizationVerificationRequestListRequestSchema, + OrganizationVerificationRequestListResponseSchema, + OrganizationVerificationRequestPostRequestSchema, +) from project.api.organizer.schemas import ( OrganizerIdSchema, OrganizerListRequestSchema, @@ -85,6 +91,9 @@ from project.api.place.schemas import ( from project.api.resources import BaseResource, require_api_access from project.models import AdminUnit, Event, PublicStatus from project.models.admin_unit import AdminUnitInvitation, AdminUnitRelation +from project.models.admin_unit_verification_request import ( + AdminUnitVerificationRequestReviewStatus, +) from project.services.admin_unit import ( get_admin_unit_invitation_query, get_admin_unit_query, @@ -105,6 +114,7 @@ from project.services.reference import ( ) from project.services.search_params import ( AdminUnitSearchParams, + AdminUnitVerificationRequestSearchParams, EventPlaceSearchParams, EventReferenceRequestSearchParams, EventReferenceSearchParams, @@ -112,11 +122,17 @@ from project.services.search_params import ( OrganizerSearchParams, TrackableSearchParams, ) +from project.services.verification import ( + admin_unit_can_verify_admin_unit, + get_verification_requests_incoming_query, + get_verification_requests_outgoing_query, +) from project.views.reference_request import ( handle_request_according_to_relation, send_reference_request_mails, ) from project.views.utils import get_current_admin_unit_for_api, send_mail_async +from project.views.verification_request import send_verification_request_inbox_mails class OrganizationResource(BaseResource): @@ -461,6 +477,76 @@ class OrganizationOutgoingEventReferenceRequestListResource(BaseResource): return reference_request, 201 +class OrganizationIncomingOrganizationVerificationRequestListResource(BaseResource): + @doc( + summary="List incoming organization verification requests of organization", + tags=["Organizations", "Organization Verification Requests"], + ) + @use_kwargs(OrganizationVerificationRequestListRequestSchema, location=("query")) + @marshal_with(OrganizationVerificationRequestListResponseSchema) + @require_api_access("organizationverificationrequest:read") + def get(self, id, **kwargs): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + + params = AdminUnitVerificationRequestSearchParams() + params.load_from_request(**kwargs) + params.target_admin_unit_id = admin_unit.id + pagination = get_verification_requests_incoming_query(params).paginate() + return pagination + + +class OrganizationOutgoingOrganizationVerificationRequestListResource(BaseResource): + @doc( + summary="List outgoing organization verification requests of organization", + tags=["Organizations", "Organization Verification Requests"], + ) + @use_kwargs(OrganizationVerificationRequestListRequestSchema, location=("query")) + @marshal_with(OrganizationVerificationRequestListResponseSchema) + @require_api_access("organizationverificationrequest:read") + def get(self, id, **kwargs): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + + params = AdminUnitVerificationRequestSearchParams() + params.load_from_request(**kwargs) + params.source_admin_unit_id = admin_unit.id + pagination = get_verification_requests_outgoing_query(params).paginate() + return pagination + + @doc( + summary="Add verification request", + tags=["Organizations", "Organization Verification Requests"], + ) + @use_kwargs( + OrganizationVerificationRequestPostRequestSchema, location="json", apply=False + ) + @marshal_with(OrganizationVerificationRequestIdSchema, 201) + @require_api_access("organizationverificationrequest:write") + def post(self, id): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + access_or_401(admin_unit, "verification_request:create") + + verification_request = self.create_instance( + OrganizationVerificationRequestPostRequestSchema, + source_admin_unit_id=admin_unit.id, + review_status=AdminUnitVerificationRequestReviewStatus.inbox, + ) + target_admin_unit = verification_request.target_admin_unit + + if not admin_unit_can_verify_admin_unit( + admin_unit, target_admin_unit + ): # pragma: no cover + abort(401) + + db.session.add(verification_request) + db.session.commit() + send_verification_request_inbox_mails(verification_request) + + return verification_request, 201 + + class OrganizationOutgoingRelationListResource(BaseResource): @doc( summary="List outgoing relations of organization", @@ -708,7 +794,6 @@ add_api_resource( "/organizations//event-references/outgoing", "api_v1_organization_outgoing_event_reference_list", ) - add_api_resource( OrganizationIncomingEventReferenceRequestListResource, "/organizations//event-reference-requests/incoming", @@ -719,7 +804,16 @@ add_api_resource( "/organizations//event-reference-requests/outgoing", "api_v1_organization_outgoing_event_reference_request_list", ) - +add_api_resource( + OrganizationIncomingOrganizationVerificationRequestListResource, + "/organizations//organization-verification-requests/incoming", + "api_v1_organization_incoming_organization_verification_request_list", +) +add_api_resource( + OrganizationOutgoingOrganizationVerificationRequestListResource, + "/organizations//organization-verification-requests/outgoing", + "api_v1_organization_outgoing_organization_verification_request_list", +) add_api_resource( OrganizationOutgoingRelationListResource, "/organizations//relations/outgoing", diff --git a/project/api/organization_verification_request/__init__.py b/project/api/organization_verification_request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/api/organization_verification_request/resources.py b/project/api/organization_verification_request/resources.py new file mode 100644 index 0000000..c652db3 --- /dev/null +++ b/project/api/organization_verification_request/resources.py @@ -0,0 +1,156 @@ +from flask import abort, make_response +from flask_apispec import doc, marshal_with, use_kwargs +from marshmallow import ValidationError + +from project import db +from project.access import access_or_401, has_access, login_api_user_or_401 +from project.api import add_api_resource +from project.api.organization_relation.schemas import OrganizationRelationIdSchema +from project.api.organization_verification_request.schemas import ( + OrganizationVerificationRequestRejectRequestSchema, + OrganizationVerificationRequestSchema, + OrganizationVerificationRequestVerifyRequestSchema, +) +from project.api.resources import BaseResource, require_api_access +from project.models import AdminUnitVerificationRequest +from project.models.admin_unit_verification_request import ( + AdminUnitVerificationRequestReviewStatus, +) +from project.services.admin_unit import upsert_admin_unit_relation +from project.views.verification_request_review import ( + send_verification_request_review_status_mails, +) + + +class OrganizationVerificationRequestResource(BaseResource): + @doc( + summary="Get organization verification request", + tags=["Organization Verification Requests"], + ) + @marshal_with(OrganizationVerificationRequestSchema) + @require_api_access("organizationverificationrequest:read") + def get(self, id): + login_api_user_or_401() + verification_request = AdminUnitVerificationRequest.query.get_or_404(id) + + if not has_access( + verification_request.source_admin_unit, "verification_request:read" + ) and not has_access( + verification_request.target_admin_unit, "verification_request:verify" + ): + abort(401) + + return verification_request + + @doc( + summary="Delete verification request", + tags=["Organization Verification Requests"], + ) + @marshal_with(None, 204) + @require_api_access("organizationverificationrequest:write") + def delete(self, id): + login_api_user_or_401() + verification_request = AdminUnitVerificationRequest.query.get_or_404(id) + access_or_401( + verification_request.source_admin_unit, "verification_request:delete" + ) + + db.session.delete(verification_request) + db.session.commit() + + return make_response("", 204) + + +class OrganizationVerificationRequestVerifyResource(BaseResource): + @doc( + summary="Verify organization verification request. Returns relation id.", + tags=["Organization Verification Requests"], + ) + @use_kwargs( + OrganizationVerificationRequestVerifyRequestSchema, location="json", apply=True + ) + @marshal_with(OrganizationRelationIdSchema, 201) + @require_api_access("organizationverificationrequest:write") + def post(self, id, **kwargs): + login_api_user_or_401() + verification_request = AdminUnitVerificationRequest.query.get_or_404(id) + access_or_401( + verification_request.target_admin_unit, "verification_request:verify" + ) + + if ( + verification_request.review_status + == AdminUnitVerificationRequestReviewStatus.verified + ): + raise ValidationError("Verification request already verified") + + verification_request.review_status = ( + AdminUnitVerificationRequestReviewStatus.verified + ) + + relation = upsert_admin_unit_relation( + verification_request.target_admin_unit_id, + verification_request.source_admin_unit_id, + ) + relation.verify = True + relation.auto_verify_event_reference_requests = kwargs.get( + "auto_verify_event_reference_requests", + relation.auto_verify_event_reference_requests, + ) + db.session.commit() + + send_verification_request_review_status_mails(verification_request) + return relation, 201 + + +class OrganizationVerificationRequestRejectResource(BaseResource): + @doc( + summary="Reject organization verification request", + tags=["Organization Verification Requests"], + ) + @use_kwargs( + OrganizationVerificationRequestRejectRequestSchema, location="json", apply=False + ) + @marshal_with(None, 204) + @require_api_access("organizationverificationrequest:write") + def post(self, id): + login_api_user_or_401() + verification_request = AdminUnitVerificationRequest.query.get_or_404(id) + access_or_401( + verification_request.target_admin_unit, "verification_request:verify" + ) + + if ( + verification_request.review_status + == AdminUnitVerificationRequestReviewStatus.verified + ): # pragma: no cover + raise ValidationError("Verification request already verified") + + verification_request = self.update_instance( + OrganizationVerificationRequestRejectRequestSchema, + instance=verification_request, + ) + verification_request.review_status = ( + AdminUnitVerificationRequestReviewStatus.rejected + ) + db.session.commit() + + send_verification_request_review_status_mails(verification_request) + return make_response("", 204) + + +add_api_resource( + OrganizationVerificationRequestResource, + "/organization-verification-request/", + "api_v1_organization_verification_request", +) +add_api_resource( + OrganizationVerificationRequestVerifyResource, + "/organization-verification-request//verify", + "api_v1_organization_verification_request_verify", +) +add_api_resource( + OrganizationVerificationRequestRejectResource, + "/organization-verification-request//reject", + "api_v1_organization_verification_request_reject", +) diff --git a/project/api/organization_verification_request/schemas.py b/project/api/organization_verification_request/schemas.py new file mode 100644 index 0000000..6539776 --- /dev/null +++ b/project/api/organization_verification_request/schemas.py @@ -0,0 +1,119 @@ +from marshmallow import fields, validate +from marshmallow_enum import EnumField + +from project.api.organization.schemas import ( + OrganizationRefSchema, + OrganizationWriteIdSchema, +) +from project.api.schemas import ( + IdSchemaMixin, + PaginationRequestSchema, + PaginationResponseSchema, + SQLAlchemyBaseSchema, + TrackableRequestSchemaMixin, + TrackableSchemaMixin, +) +from project.models import AdminUnitVerificationRequest +from project.models.admin_unit_verification_request import ( + AdminUnitVerificationRequestRejectionReason, + AdminUnitVerificationRequestReviewStatus, +) + + +class OrganizationVerificationRequestModelSchema(SQLAlchemyBaseSchema): + class Meta: + model = AdminUnitVerificationRequest + load_instance = True + + +class OrganizationVerificationRequestIdSchema( + OrganizationVerificationRequestModelSchema, IdSchemaMixin +): + pass + + +class OrganizationVerificationRequestBaseSchemaMixin(TrackableSchemaMixin): + review_status = EnumField( + AdminUnitVerificationRequestReviewStatus, + load_default=AdminUnitVerificationRequestReviewStatus.inbox, + ) + rejection_reason = EnumField( + AdminUnitVerificationRequestRejectionReason, + ) + + +class OrganizationVerificationRequestRefSchema( + OrganizationVerificationRequestIdSchema, TrackableSchemaMixin +): + source_organization = fields.Nested( + OrganizationRefSchema, attribute="source_admin_unit" + ) + target_organization = fields.Nested( + OrganizationRefSchema, attribute="target_admin_unit" + ) + + +class OrganizationVerificationRequestSchema( + OrganizationVerificationRequestIdSchema, + OrganizationVerificationRequestBaseSchemaMixin, +): + source_organization = fields.Nested( + OrganizationRefSchema, attribute="source_admin_unit" + ) + target_organization = fields.Nested( + OrganizationRefSchema, attribute="target_admin_unit" + ) + + +class OrganizationVerificationRequestListRequestSchema( + PaginationRequestSchema, TrackableRequestSchemaMixin +): + sort = fields.Str( + metadata={"description": "Sort result items."}, + validate=validate.OneOf(["-created_at", "-updated_at", "-last_modified_at"]), + ) + + +class OrganizationVerificationRequestListResponseSchema(PaginationResponseSchema): + items = fields.List( + fields.Nested(OrganizationVerificationRequestRefSchema), + metadata={"description": "Organization verification requests"}, + ) + + +class OrganizationVerificationRequestWriteSchemaMixin(object): + target_organization = fields.Nested( + OrganizationWriteIdSchema, + attribute="target_admin_unit", + required=True, + metadata={"description": "Target organization."}, + ) + + +class OrganizationVerificationRequestPostRequestSchema( + OrganizationVerificationRequestModelSchema, + OrganizationVerificationRequestWriteSchemaMixin, +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + +class OrganizationVerificationRequestVerifyRequestSchema(SQLAlchemyBaseSchema): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + auto_verify_event_reference_requests = fields.Bool() + + +class OrganizationVerificationRequestRejectRequestSchema( + OrganizationVerificationRequestModelSchema, +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + rejection_reason = EnumField( + AdminUnitVerificationRequestRejectionReason, + ) diff --git a/project/services/search_params.py b/project/services/search_params.py index 1b43f29..1f2973d 100644 --- a/project/services/search_params.py +++ b/project/services/search_params.py @@ -70,6 +70,14 @@ class EventReferenceRequestSearchParams(TrackableSearchParams): self.review_status = None +class AdminUnitVerificationRequestSearchParams(TrackableSearchParams): + def __init__(self): + super().__init__() + self.source_admin_unit_id = None + self.target_admin_unit_id = None + self.review_status = None + + class AdminUnitSearchParams(TrackableSearchParams): def __init__(self): super().__init__() diff --git a/project/services/verification.py b/project/services/verification.py index c6c4ffc..b881c53 100644 --- a/project/services/verification.py +++ b/project/services/verification.py @@ -5,18 +5,54 @@ from project.models import ( AdminUnitVerificationRequest, AdminUnitVerificationRequestReviewStatus, ) +from project.models.admin_unit import AdminUnit +from project.services.search_params import AdminUnitVerificationRequestSearchParams -def get_verification_requests_incoming_query(admin_unit): - return AdminUnitVerificationRequest.query.filter( - and_( - AdminUnitVerificationRequest.review_status - != AdminUnitVerificationRequestReviewStatus.verified, - AdminUnitVerificationRequest.target_admin_unit_id == admin_unit.id, - ) +def admin_unit_can_verify_admin_unit( + source_admin_unit: AdminUnit, target_admin_unit: AdminUnit +): + return ( + target_admin_unit.id != source_admin_unit.id + and target_admin_unit.can_verify_other + and target_admin_unit.incoming_verification_requests_allowed ) +def get_verification_requests_incoming_query( + params: AdminUnitVerificationRequestSearchParams, +): + result = AdminUnitVerificationRequest.query + + if params.target_admin_unit_id: + result = result.filter( + AdminUnitVerificationRequest.target_admin_unit_id + == params.target_admin_unit_id + ) + + result = params.get_trackable_query(result, AdminUnitVerificationRequest) + result = params.get_trackable_order_by(result, AdminUnitVerificationRequest) + result = result.order_by(AdminUnitVerificationRequest.created_at.desc()) + return result + + +def get_verification_requests_outgoing_query( + params: AdminUnitVerificationRequestSearchParams, +): + result = AdminUnitVerificationRequest.query + + if params.source_admin_unit_id: + result = result.filter( + AdminUnitVerificationRequest.source_admin_unit_id + == params.source_admin_unit_id + ) + + result = params.get_trackable_query(result, AdminUnitVerificationRequest) + result = params.get_trackable_order_by(result, AdminUnitVerificationRequest) + result = result.order_by(AdminUnitVerificationRequest.created_at.desc()) + return result + + def get_verification_requests_incoming_badge_query(admin_unit): return AdminUnitVerificationRequest.query.options( load_only(AdminUnitVerificationRequest.id) diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index d3fdc0f..fd62890 100644 Binary files a/project/translations/de/LC_MESSAGES/messages.mo and b/project/translations/de/LC_MESSAGES/messages.mo differ diff --git a/project/translations/de/LC_MESSAGES/messages.po b/project/translations/de/LC_MESSAGES/messages.po index 0cc1428..25c6b36 100644 --- a/project/translations/de/LC_MESSAGES/messages.po +++ b/project/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-07-09 21:57+0200\n" +"POT-Creation-Date: 2023-07-13 15:39+0200\n" "PO-Revision-Date: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -202,7 +202,7 @@ msgstr "." msgid "message" msgstr "message" -#: project/api/organization/resources.py:479 +#: project/api/organization/resources.py:645 #: project/views/admin_unit_member_invitation.py:89 msgid "You have received an invitation" msgstr "Du hast eine Einladung erhalten" @@ -2639,7 +2639,7 @@ msgstr "Organisation erfolgreich aktualisiert" #: project/views/admin.py:85 project/views/admin_unit.py:202 #: project/views/admin_unit.py:235 project/views/manage.py:315 -#: project/views/verification_request.py:167 +#: project/views/verification_request.py:168 msgid "Entered name does not match organization name" msgstr "Der eingegebene Name entspricht nicht dem Namen der Organisation" @@ -2731,31 +2731,31 @@ msgstr "Die eingegebene Email passt nicht zur Email der Einladung" msgid "Invitation successfully deleted" msgstr "Einladung erfolgreich gelöscht" -#: project/views/event.py:205 +#: project/views/event.py:208 msgid "Event successfully published" msgstr "Veranstaltung erfolgreich veröffentlicht" -#: project/views/event.py:207 +#: project/views/event.py:210 msgid "Draft successfully saved" msgstr "Entwurf erfolgreich gespeichert" -#: project/views/event.py:209 +#: project/views/event.py:212 msgid "Event successfully planned" msgstr "Veranstaltung erfolgreich geplant" -#: project/views/event.py:266 +#: project/views/event.py:272 msgid "Event successfully updated" msgstr "Veranstaltung erfolgreich aktualisiert" -#: project/views/event.py:292 +#: project/views/event.py:298 msgid "Event successfully deleted" msgstr "Veranstaltung erfolgreich gelöscht" -#: project/views/event.py:457 +#: project/views/event.py:463 msgid "Referenced event changed" msgstr "Empfohlene Veranstaltung wurde geändert" -#: project/views/event.py:480 +#: project/views/event.py:486 msgid "New event report" msgstr "Neue Meldung zu einer Veranstaltung" @@ -2780,7 +2780,7 @@ msgid "Event suggestion successfully rejected" msgstr "Veranstaltungsvorschlag erfolgreich abgelehnt" #: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:135 +#: project/views/reference_request_review.py:129 msgid "Event review status updated" msgstr "Prüfungsstatus aktualisiert" @@ -2862,16 +2862,16 @@ msgstr "Veranstaltung erfolgreich empfohlen" msgid "Reference successfully updated" msgstr "Empfehlung erfolgreich empfohlen" -#: project/views/reference.py:153 +#: project/views/reference.py:156 msgid "Reference successfully deleted" msgstr "Empfehlung erfolgreich gelöscht" -#: project/views/reference_request.py:138 +#: project/views/reference_request.py:145 #, python-format msgid "%(organization)s accepted your reference request" msgstr "%(organization)s hat deine Empfehlungsanfrage akzeptiert" -#: project/views/reference_request.py:145 +#: project/views/reference_request.py:152 #, python-format msgid "" "Reference request to %(organization)s successfully created. You will be " @@ -2881,29 +2881,28 @@ msgstr "" "benachrichtigt, nachdem die andere Organisation die Veranstaltung geprüft" " hat." -#: project/views/reference_request.py:168 +#: project/views/reference_request.py:172 msgid "New reference request" msgstr "Neue Empfehlungsanfrage" -#: project/views/reference_request.py:177 +#: project/views/reference_request.py:181 msgid "New reference automatically verified" msgstr "Neue automatisch verifizierte Empfehlung" #: project/views/reference_request_review.py:34 -#: project/views/verification_request_review.py:28 msgid "Request already verified" msgstr "Empfehlungsanfrage ist bereits verifiziert" -#: project/views/reference_request_review.py:57 +#: project/views/reference_request_review.py:51 msgid "Reference successfully created" msgstr "Empfehlung erfolgreich erstellt" -#: project/views/reference_request_review.py:65 +#: project/views/reference_request_review.py:59 msgid "Request successfully updated" msgstr "Empfehlungsanfrage erfolgreich aktualisiert" -#: project/views/reference_request_review.py:88 -#: project/views/verification_request_review.py:79 +#: project/views/reference_request_review.py:82 +#: project/views/verification_request_review.py:73 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " @@ -2957,7 +2956,7 @@ msgstr "" "Die Einladung wurde für einen anderen Nutzer ausgestellt. Melde dich mit " "der Email-Adresse an, an die die Einladung geschickt wurde." -#: project/views/verification_request.py:131 +#: project/views/verification_request.py:132 msgid "" "Request successfully created. You will be notified after the other " "organization reviewed the request." @@ -2965,23 +2964,27 @@ msgstr "" "Verifizierungsanfrage erfolgreich erstellt. Du wirst benachrichtigt, " "nachdem die andere Organisation die Anfrage geprüft hat." -#: project/views/verification_request.py:172 +#: project/views/verification_request.py:173 msgid "Verification request successfully deleted" msgstr "Verifizierungsanfrage erfolgreich gelöscht" -#: project/views/verification_request.py:205 +#: project/views/verification_request.py:206 msgid "New verification request" msgstr "Neue Verifizierungsanfrage" -#: project/views/verification_request_review.py:60 +#: project/views/verification_request_review.py:28 +msgid "Verification request already verified" +msgstr "Verifizierungsanfrage ist bereits verifiziert" + +#: project/views/verification_request_review.py:54 msgid "Organization successfully verified" msgstr "Organisation erfolgreich verifiziert" -#: project/views/verification_request_review.py:62 +#: project/views/verification_request_review.py:56 msgid "Verification request successfully updated" msgstr "Verifizierungsanfrage erfolgreich aktualisiert" -#: project/views/verification_request_review.py:115 +#: project/views/verification_request_review.py:109 msgid "Verification request review status updated" msgstr "Prüfungsstatus der Verifizierungsanfrage aktualisiert" diff --git a/project/translations/en/LC_MESSAGES/messages.mo b/project/translations/en/LC_MESSAGES/messages.mo index 1fe580e..61ad21b 100644 Binary files a/project/translations/en/LC_MESSAGES/messages.mo and b/project/translations/en/LC_MESSAGES/messages.mo differ diff --git a/project/translations/en/LC_MESSAGES/messages.po b/project/translations/en/LC_MESSAGES/messages.po index 09f3dcd..86ab62e 100644 --- a/project/translations/en/LC_MESSAGES/messages.po +++ b/project/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-07-09 21:57+0200\n" +"POT-Creation-Date: 2023-07-13 15:39+0200\n" "PO-Revision-Date: 2021-04-30 15:04+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -202,7 +202,7 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:479 +#: project/api/organization/resources.py:645 #: project/views/admin_unit_member_invitation.py:89 msgid "You have received an invitation" msgstr "" @@ -2569,7 +2569,7 @@ msgstr "" #: project/views/admin.py:85 project/views/admin_unit.py:202 #: project/views/admin_unit.py:235 project/views/manage.py:315 -#: project/views/verification_request.py:167 +#: project/views/verification_request.py:168 msgid "Entered name does not match organization name" msgstr "" @@ -2658,31 +2658,31 @@ msgstr "" msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:205 +#: project/views/event.py:208 msgid "Event successfully published" msgstr "" -#: project/views/event.py:207 +#: project/views/event.py:210 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:209 +#: project/views/event.py:212 msgid "Event successfully planned" msgstr "" -#: project/views/event.py:266 +#: project/views/event.py:272 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:292 +#: project/views/event.py:298 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:457 +#: project/views/event.py:463 msgid "Referenced event changed" msgstr "" -#: project/views/event.py:480 +#: project/views/event.py:486 msgid "New event report" msgstr "" @@ -2707,7 +2707,7 @@ msgid "Event suggestion successfully rejected" msgstr "" #: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:135 +#: project/views/reference_request_review.py:129 msgid "Event review status updated" msgstr "" @@ -2787,45 +2787,44 @@ msgstr "" msgid "Reference successfully updated" msgstr "" -#: project/views/reference.py:153 +#: project/views/reference.py:156 msgid "Reference successfully deleted" msgstr "" -#: project/views/reference_request.py:138 +#: project/views/reference_request.py:145 #, python-format msgid "%(organization)s accepted your reference request" msgstr "" -#: project/views/reference_request.py:145 +#: project/views/reference_request.py:152 #, python-format msgid "" "Reference request to %(organization)s successfully created. You will be " "notified after the other organization reviews the event." msgstr "" -#: project/views/reference_request.py:168 +#: project/views/reference_request.py:172 msgid "New reference request" msgstr "" -#: project/views/reference_request.py:177 +#: project/views/reference_request.py:181 msgid "New reference automatically verified" msgstr "" #: project/views/reference_request_review.py:34 -#: project/views/verification_request_review.py:28 msgid "Request already verified" msgstr "" -#: project/views/reference_request_review.py:57 +#: project/views/reference_request_review.py:51 msgid "Reference successfully created" msgstr "" -#: project/views/reference_request_review.py:65 +#: project/views/reference_request_review.py:59 msgid "Request successfully updated" msgstr "" -#: project/views/reference_request_review.py:88 -#: project/views/verification_request_review.py:79 +#: project/views/reference_request_review.py:82 +#: project/views/verification_request_review.py:73 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " @@ -2871,29 +2870,33 @@ msgid "" " the invitation was sent to." msgstr "" -#: project/views/verification_request.py:131 +#: project/views/verification_request.py:132 msgid "" "Request successfully created. You will be notified after the other " "organization reviewed the request." msgstr "" -#: project/views/verification_request.py:172 +#: project/views/verification_request.py:173 msgid "Verification request successfully deleted" msgstr "" -#: project/views/verification_request.py:205 +#: project/views/verification_request.py:206 msgid "New verification request" msgstr "" -#: project/views/verification_request_review.py:60 +#: project/views/verification_request_review.py:28 +msgid "Verification request already verified" +msgstr "" + +#: project/views/verification_request_review.py:54 msgid "Organization successfully verified" msgstr "" -#: project/views/verification_request_review.py:62 +#: project/views/verification_request_review.py:56 msgid "Verification request successfully updated" msgstr "" -#: project/views/verification_request_review.py:115 +#: project/views/verification_request_review.py:109 msgid "Verification request review status updated" msgstr "" diff --git a/project/views/verification_request.py b/project/views/verification_request.py index 91c8792..aa8090a 100644 --- a/project/views/verification_request.py +++ b/project/views/verification_request.py @@ -15,8 +15,15 @@ from project.models import ( ) from project.models.admin_unit import AdminUnit from project.services.admin_unit import get_admin_unit_query -from project.services.search_params import AdminUnitSearchParams -from project.services.verification import get_verification_requests_incoming_query +from project.services.search_params import ( + AdminUnitSearchParams, + AdminUnitVerificationRequestSearchParams, +) +from project.services.verification import ( + admin_unit_can_verify_admin_unit, + get_verification_requests_incoming_query, + get_verification_requests_outgoing_query, +) from project.views.utils import ( flash_errors, get_pagination_urls, @@ -32,11 +39,10 @@ from project.views.utils import ( @manage_required("verification_request:read") def manage_admin_unit_verification_requests_incoming(id): admin_unit = g.manage_admin_unit - requests = ( - get_verification_requests_incoming_query(admin_unit) - .order_by(AdminUnitVerificationRequest.created_at.desc()) - .paginate() - ) + + params = AdminUnitVerificationRequestSearchParams() + params.target_admin_unit_id = admin_unit.id + requests = get_verification_requests_incoming_query(params).paginate() return render_template( "manage/verification_requests_incoming.html", @@ -51,13 +57,10 @@ def manage_admin_unit_verification_requests_incoming(id): @manage_required("verification_request:read") def manage_admin_unit_verification_requests_outgoing(id): admin_unit = g.manage_admin_unit - requests = ( - AdminUnitVerificationRequest.query.filter( - AdminUnitVerificationRequest.source_admin_unit_id == admin_unit.id - ) - .order_by(AdminUnitVerificationRequest.created_at.desc()) - .paginate() - ) + + params = AdminUnitVerificationRequestSearchParams() + params.target_admin_unit_id = admin_unit.id + requests = get_verification_requests_outgoing_query(params).paginate() if not admin_unit.is_verified and requests.total == 0: return redirect( @@ -103,10 +106,8 @@ def manage_admin_unit_verification_requests_outgoing_create(id, target_id): admin_unit = g.manage_admin_unit target_admin_unit = AdminUnit.query.get_or_404(target_id) - if ( - target_admin_unit.id == admin_unit.id - or not target_admin_unit.can_verify_other - or not target_admin_unit.incoming_verification_requests_allowed + if not admin_unit_can_verify_admin_unit( + admin_unit, target_admin_unit ): # pragma: no cover return redirect( url_for( diff --git a/project/views/verification_request_review.py b/project/views/verification_request_review.py index 789d011..8741d78 100644 --- a/project/views/verification_request_review.py +++ b/project/views/verification_request_review.py @@ -25,7 +25,7 @@ def admin_unit_verification_request_review(id): access_or_401(request.target_admin_unit, "verification_request:verify") if request.review_status == AdminUnitVerificationRequestReviewStatus.verified: - flash(gettext("Request already verified"), "danger") + flash(gettext("Verification request already verified"), "danger") return redirect( url_for( "manage_admin_unit_verification_requests_incoming", diff --git a/tests/api/test_organization.py b/tests/api/test_organization.py index 529b2bc..067226e 100644 --- a/tests/api/test_organization.py +++ b/tests/api/test_organization.py @@ -662,6 +662,87 @@ def test_reference_requests_outgoing_post_draft( utils.assert_response_unauthorized(response) +def test_organization_verification_requests_incoming( + client, seeder: Seeder, utils: UtilActions +): + user_id, admin_unit_id = seeder.setup_api_access() + seeder.create_incoming_admin_unit_verification_request(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_incoming_organization_verification_request_list", + id=admin_unit_id, + ) + utils.get_json_ok(url) + + +def test_organization_verification_requests_outgoing( + client, seeder: Seeder, utils: UtilActions +): + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario(api=True) + seeder.create_admin_unit_verification_request( + unverified_admin_unit_id, verifier_admin_unit_id + ) + + url = utils.get_url( + "api_v1_organization_outgoing_organization_verification_request_list", + id=unverified_admin_unit_id, + ) + utils.get_json_ok(url) + + +def test_organization_verification_requests_outgoing_post( + client, app, seeder: Seeder, utils: UtilActions, db, mocker +): + mail_mock = utils.mock_send_mails_async(mocker) + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario(api=True) + + url = utils.get_url( + "api_v1_organization_outgoing_organization_verification_request_list", + id=unverified_admin_unit_id, + ) + data = { + "target_organization": {"id": verifier_admin_unit_id}, + } + + response = utils.post_json(url, data) + utils.assert_response_created(response) + assert "id" in response.json + utils.assert_send_mail_called(mail_mock, "test@test.de") + + with app.app_context(): + from project.models import ( + AdminUnitVerificationRequest, + AdminUnitVerificationRequestReviewStatus, + ) + + organization_verification_request = db.session.get( + AdminUnitVerificationRequest, int(response.json["id"]) + ) + assert organization_verification_request is not None + assert ( + organization_verification_request.source_admin_unit_id + == unverified_admin_unit_id + ) + assert ( + organization_verification_request.target_admin_unit_id + == verifier_admin_unit_id + ) + assert ( + organization_verification_request.review_status + == AdminUnitVerificationRequestReviewStatus.inbox + ) + + def test_outgoing_relation_read(client, seeder: Seeder, utils: UtilActions): user_id, admin_unit_id = seeder.setup_api_access() ( diff --git a/tests/api/test_organization_verification_request.py b/tests/api/test_organization_verification_request.py new file mode 100644 index 0000000..67cc4d7 --- /dev/null +++ b/tests/api/test_organization_verification_request.py @@ -0,0 +1,180 @@ +from tests.seeder import Seeder +from tests.utils import UtilActions + + +def test_read(client, seeder: Seeder, utils: UtilActions): + user_id, admin_unit_id = seeder.setup_api_access() + ( + other_user_id, + other_admin_unit_id, + request_id, + ) = seeder.create_incoming_admin_unit_verification_request(admin_unit_id) + + url = utils.get_url("api_v1_organization_verification_request", id=request_id) + utils.get_json_ok(url) + + +def test_read_noAccess(client, seeder: Seeder, utils: UtilActions): + user_id, admin_unit_id = seeder.setup_api_access() + third_user_id = seeder.create_user("third@test.de") + third_admin_unit_id = seeder.create_admin_unit(third_user_id, "Third Crew") + ( + other_user_id, + other_admin_unit_id, + request_id, + ) = seeder.create_incoming_admin_unit_verification_request(third_admin_unit_id) + + url = utils.get_url("api_v1_organization_verification_request", id=request_id) + response = utils.get_json(url) + utils.assert_response_unauthorized(response) + + +def test_delete(client, app, db, seeder: Seeder, utils: UtilActions): + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario(api=True) + reference_request_id = seeder.create_admin_unit_verification_request( + unverified_admin_unit_id, verifier_admin_unit_id + ) + + url = utils.get_url( + "api_v1_organization_verification_request", id=reference_request_id + ) + response = utils.delete(url) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import EventReferenceRequest + + reference = db.session.get(EventReferenceRequest, reference_request_id) + assert reference is None + + +def test_verify(client, app, db, seeder: Seeder, utils: UtilActions, mocker): + mail_mock = utils.mock_send_mails_async(mocker) + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario( + log_in_verifier=True, api=True + ) + reference_request_id = seeder.create_admin_unit_verification_request( + unverified_admin_unit_id, verifier_admin_unit_id + ) + + url = utils.get_url( + "api_v1_organization_verification_request_verify", id=reference_request_id + ) + data = { + "auto_verify_event_reference_requests": True, + } + response = utils.post_json(url, data) + utils.assert_response_created(response) + assert "id" in response.json + utils.assert_send_mail_called(mail_mock, "mitglied@verein.de") + + with app.app_context(): + from project.models import AdminUnitVerificationRequest + from project.services.admin_unit import get_admin_unit_relation + + verification_request = db.session.get( + AdminUnitVerificationRequest, reference_request_id + ) + assert verification_request.verified + + relation = get_admin_unit_relation( + verifier_admin_unit_id, unverified_admin_unit_id + ) + assert relation is not None + assert relation.id == int(response.json["id"]) + assert relation.verify + assert relation.auto_verify_event_reference_requests + + +def test_verify_alreadyVerified( + client, app, db, seeder: Seeder, utils: UtilActions, mocker +): + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario( + log_in_verifier=True, api=True + ) + reference_request_id = seeder.create_admin_unit_verification_request( + unverified_admin_unit_id, verifier_admin_unit_id + ) + + with app.app_context(): + from project.models import ( + AdminUnitVerificationRequest, + AdminUnitVerificationRequestReviewStatus, + ) + + verification_request = db.session.get( + AdminUnitVerificationRequest, reference_request_id + ) + + verification_request.review_status = ( + AdminUnitVerificationRequestReviewStatus.verified + ) + db.session.commit() + + url = utils.get_url( + "api_v1_organization_verification_request_verify", id=reference_request_id + ) + data = { + "auto_verify_event_reference_requests": True, + } + response = utils.post_json(url, data) + utils.assert_response_unprocessable_entity(response) + + +def test_reject(client, app, db, seeder: Seeder, utils: UtilActions, mocker): + mail_mock = utils.mock_send_mails_async(mocker) + ( + verifier_user_id, + verifier_admin_unit_id, + unverified_user_id, + unverified_admin_unit_id, + ) = seeder.setup_admin_unit_missing_verification_scenario( + log_in_verifier=True, api=True + ) + reference_request_id = seeder.create_admin_unit_verification_request( + unverified_admin_unit_id, verifier_admin_unit_id + ) + + url = utils.get_url( + "api_v1_organization_verification_request_reject", id=reference_request_id + ) + data = { + "rejection_reason": "unknown", + } + response = utils.post_json(url, data) + utils.assert_response_no_content(response) + utils.assert_send_mail_called(mail_mock, "mitglied@verein.de") + + with app.app_context(): + from project.models import ( + AdminUnitVerificationRequest, + AdminUnitVerificationRequestRejectionReason, + AdminUnitVerificationRequestReviewStatus, + ) + + verification_request = db.session.get( + AdminUnitVerificationRequest, reference_request_id + ) + assert ( + verification_request.review_status + == AdminUnitVerificationRequestReviewStatus.rejected + ) + assert ( + verification_request.rejection_reason + == AdminUnitVerificationRequestRejectionReason.unknown + ) diff --git a/tests/seeder.py b/tests/seeder.py index 633135f..8afb212 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -305,9 +305,16 @@ class Seeder(object): return client_id - def setup_api_access(self, admin=True, admin_unit_verified=True, user_access=True): + def setup_api_access( + self, + admin=True, + admin_unit_verified=True, + user_access=True, + email="test@test.de", + admin_unit_name="Meine Crew", + ): user_id, admin_unit_id = self.setup_base( - admin=admin, log_in=False, admin_unit_verified=admin_unit_verified + admin, False, admin_unit_verified, email, admin_unit_name ) if user_access: @@ -322,13 +329,17 @@ class Seeder(object): with self._app.app_context(): from project.models import OAuth2Client + from project.services.user import get_user + + user = get_user(user_id) + email = user.email oauth2_client = self._db.session.get(OAuth2Client, oauth2_client_id) client_id = oauth2_client.client_id client_secret = oauth2_client.client_secret scope = oauth2_client.scope - self._utils.login(follow_redirects=False) + self._utils.login(email=email, follow_redirects=False) self._utils.authorize(client_id, client_secret, scope) self._utils.logout() return (user_id, admin_unit_id) @@ -673,7 +684,9 @@ class Seeder(object): ) return (other_user_id, other_admin_unit_id, relation_id) - def setup_admin_unit_missing_verification_scenario(self, log_in_verifier=False): + def setup_admin_unit_missing_verification_scenario( + self, log_in_verifier=False, api=False + ): verifier_user_id = self.create_user() verifier_admin_unit_id = self.create_admin_unit( verifier_user_id, @@ -684,15 +697,26 @@ class Seeder(object): incoming_verification_requests_text="Please give us a call", ) - unverified_user_id, unverified_admin_unit_id = self.setup_base( - log_in=not log_in_verifier, - admin_unit_verified=False, - email="mitglied@verein.de", - name="Verein", - ) + if api: + unverified_user_id, unverified_admin_unit_id = self.setup_api_access( + admin=False, + admin_unit_verified=False, + email="mitglied@verein.de", + admin_unit_name="Verein", + ) - if log_in_verifier: - self._utils.login() + if log_in_verifier: + self.authorize_api_access(verifier_user_id, verifier_admin_unit_id) + else: + unverified_user_id, unverified_admin_unit_id = self.setup_base( + log_in=not log_in_verifier, + admin_unit_verified=False, + email="mitglied@verein.de", + name="Verein", + ) + + if log_in_verifier: + self._utils.login() return ( verifier_user_id,