From 2c2b95918dabf7a9d6ca8c411be65b6ba4260cdb Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 25 Jun 2023 22:35:26 +0200 Subject: [PATCH] Organisation-Page: Link to verification #501 Open --- project/access.py | 2 +- project/api/organization/resources.py | 26 ++++ project/api/organization/schemas.py | 1 + project/jinja_filters.py | 50 ++++++++ project/static/vue/common/typeahead.vue.js | 13 +- .../vue/organization-relations/create.vue.js | 18 ++- project/static/vue/organization/list.vue.js | 2 +- project/static/vue/organization/read.vue.js | 117 ++++++++++++++++-- project/templates/layout_vue.html | 65 ++++++++++ tests/api/test_organization.py | 20 +++ 10 files changed, 298 insertions(+), 16 deletions(-) diff --git a/project/access.py b/project/access.py index 62d5d13..f481de2 100644 --- a/project/access.py +++ b/project/access.py @@ -225,7 +225,7 @@ def can_verify_admin_unit(): if not current_user.is_authenticated: # pragma: no cover return False - if has_current_user_role("admin"): + if has_current_user_role("admin"): # pragma: no cover return True admin_units = get_admin_units_for_manage() diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 83ce142..997d2f9 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -61,6 +61,7 @@ from project.api.organization_relation.schemas import ( OrganizationRelationIdSchema, OrganizationRelationListRequestSchema, OrganizationRelationListResponseSchema, + OrganizationRelationSchema, ) from project.api.organizer.schemas import ( OrganizerIdSchema, @@ -76,6 +77,7 @@ 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 AdminUnitRelation from project.services.admin_unit import ( get_admin_unit_invitation_query, get_admin_unit_query, @@ -395,6 +397,25 @@ class OrganizationOutgoingRelationListResource(BaseResource): return relation, 201 +class OrganizationOutgoingRelationResource(BaseResource): + @doc( + summary="Get outgoing relation to given target organization", + tags=["Organizations", "Organization Relations"], + ) + @marshal_with(OrganizationRelationSchema) + @require_api_access("organization:read") + def get(self, id, target_id): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + access_or_401(admin_unit, "admin_unit:update") + + relation = AdminUnitRelation.query.filter( + AdminUnitRelation.source_admin_unit_id == id, + AdminUnitRelation.target_admin_unit_id == target_id, + ).first_or_404(id) + return relation + + class OrganizationOrganizationInvitationListResource(BaseResource): @doc( summary="List organization invitations of organization", @@ -586,6 +607,11 @@ add_api_resource( "/organizations//relations/outgoing", "api_v1_organization_outgoing_relation_list", ) +add_api_resource( + OrganizationOutgoingRelationResource, + "/organizations//relations/outgoing/", + "api_v1_organization_outgoing_relation", +) add_api_resource( OrganizationOrganizationInvitationListResource, "/organizations//organization-invitations", diff --git a/project/api/organization/schemas.py b/project/api/organization/schemas.py index c17f779..9bfe2f5 100644 --- a/project/api/organization/schemas.py +++ b/project/api/organization/schemas.py @@ -68,6 +68,7 @@ class OrganizationRefSchema(OrganizationIdSchema): class OrganizationListRefSchema(OrganizationRefSchema): short_name = marshmallow.auto_field() + is_verified = fields.Boolean() class OrganizationListRequestSchema(PaginationRequestSchema): diff --git a/project/jinja_filters.py b/project/jinja_filters.py index 7a29a79..e7763b4 100644 --- a/project/jinja_filters.py +++ b/project/jinja_filters.py @@ -109,9 +109,59 @@ def get_context_processors(): return has_tos() + def get_current_user_roles(): + from flask_security import current_user + + if not current_user.is_authenticated: # pragma: no cover + return [] + + return [r.name for r in current_user.roles] + + def get_current_user_permissions(): + from flask_security import current_user + + if not current_user.is_authenticated: # pragma: no cover + return [] + + return sum([r.permissions for r in current_user.roles], []) + + def get_current_admin_unit_roles(): + from project.access import get_current_user_member_for_admin_unit + + current_admin_unit = get_current_admin_unit() + + if not current_admin_unit: # pragma: no cover + return [] + + member = get_current_user_member_for_admin_unit(current_admin_unit.id) + + if not member: # pragma: no cover + return [] + + return [r.name for r in member.roles] + + def get_current_admin_unit_permissions(): + from project.access import get_current_user_member_for_admin_unit + + current_admin_unit = get_current_admin_unit() + + if not current_admin_unit: # pragma: no cover + return [] + + member = get_current_user_member_for_admin_unit(current_admin_unit.id) + + if not member: # pragma: no cover + return [] + + return sum([r.permissions for r in member.roles], []) + return dict( current_admin_unit=get_current_admin_unit(), + get_current_admin_unit_roles=get_current_admin_unit_roles, + get_current_admin_unit_permissions=get_current_admin_unit_permissions, get_manage_menu_options=get_manage_menu_options, has_access=has_access, has_tos=has_tos, + get_current_user_roles=get_current_user_roles, + get_current_user_permissions=get_current_user_permissions, ) diff --git a/project/static/vue/common/typeahead.vue.js b/project/static/vue/common/typeahead.vue.js index 8692812..9bca0d7 100644 --- a/project/static/vue/common/typeahead.vue.js +++ b/project/static/vue/common/typeahead.vue.js @@ -102,10 +102,17 @@ const CustomTypeahead = { ); }, watch: { + // Handles internal model changes. selected(newVal) { - this.$emit("input", newVal) - this.$refs.validationProvider.syncValue(newVal) - this.$refs.validationProvider.validate() + this.$emit("input", newVal); + this.$refs.validationProvider.syncValue(newVal); + this.$refs.validationProvider.validate(); + }, + // Handles external model changes. + value(newVal) { + this.selected = newVal; + this.$refs.typeahead.inputValue = + newVal != null ? this.$refs.typeahead.serializer(newVal) : null; } } }; diff --git a/project/static/vue/organization-relations/create.vue.js b/project/static/vue/organization-relations/create.vue.js index 1749e37..4ae0800 100644 --- a/project/static/vue/organization-relations/create.vue.js +++ b/project/static/vue/organization-relations/create.vue.js @@ -73,7 +73,7 @@ const OrganizationRelationCreate = { this.form = { targetOrganization: null, auto_verify_event_reference_requests: false, - verify: false, + verify: this.$route.query.verify == "1", } this.loadAdminUnit(); }, @@ -86,6 +86,22 @@ const OrganizationRelationCreate = { }) .then((response) => { this.adminUnit = response.data; + this.loadTarget(); + }); + }, + loadTarget() { + if (this.$route.query.target == undefined) { + return; + } + axios + .get(`/api/v1/organizations/${this.$route.query.target}`, { + withCredentials: true, + }) + .then((response) => { + this.form.targetOrganization = { + id: response.data.id, + name: response.data.name + }; }); }, handleLoadingAdminUnit(isLoading) { diff --git a/project/static/vue/organization/list.vue.js b/project/static/vue/organization/list.vue.js index 401ce1b..0084ac0 100644 --- a/project/static/vue/organization/list.vue.js +++ b/project/static/vue/organization/list.vue.js @@ -37,7 +37,7 @@ const OrganizationList = { > diff --git a/project/static/vue/organization/read.vue.js b/project/static/vue/organization/read.vue.js index 6afbfe5..9b0425f 100644 --- a/project/static/vue/organization/read.vue.js +++ b/project/static/vue/organization/read.vue.js @@ -40,19 +40,59 @@ const OrganizationRead = {
- {{ organization.description }} + {{ organization.description }} +
+ +
+ +
+ + +
+ {{ $t('comp.relationVerify', { source: $root.currentAdminUnit.name, target: organization.name }) }} +
+
+ {{ $t('comp.relationAutoVerifyEventReferenceRequests', { source: $root.currentAdminUnit.name, target: organization.name }) }} +
+
+ + {{ $t("comp.relationEdit") }} + +
+ + +
+
- - - {{ $t("shared.models.event.listName") }} - - - - {{ $t("comp.icalExport") }} - - + + + {{ $t("shared.models.event.listName") }} + + + + {{ $t("comp.icalExport") }} + + + @@ -81,6 +122,13 @@ const OrganizationRead = { download: "Download", icalCopied: "Link copied", icalExport: "iCal calendar", + organizationNotVerified: "{organization} is not verified", + relationVerify: "{source} verifies {target}", + relationAutoVerifyEventReferenceRequests: "{source} verifies reference requests from {target} automatically", + relationDoesNotExist: "There is no relation from {source} to {target}", + relationEdit: "Edit relation", + relationCreate: "Create relation", + relationCreateToVerify: "Verify {organization}", }, }, de: { @@ -89,13 +137,24 @@ const OrganizationRead = { download: "Runterladen", icalCopied: "Link kopiert", icalExport: "iCal Kalender", + organizationNotVerified: "{organization} ist nicht verifiziert", + relationVerify: "{source} verifiziert {target}", + relationAutoVerifyEventReferenceRequests: "{source} verifiziert Empfehlungsanfragen von {target} automatisch", + relationDoesNotExist: "Es besteht keine Beziehung von {source} zu {target}", + relationEdit: "Beziehung bearbeiten", + relationCreate: "Beziehung erstellen", + relationCreateToVerify: "Verifiziere {organization}", }, }, }, }, data: () => ({ isLoading: false, + isLoadingRelation: false, organization: null, + relation: null, + canRelation: false, + relationDoesNotExist: false, }), computed: { organizationId() { @@ -107,11 +166,22 @@ const OrganizationRead = { icalDocsUrl() { return this.$root.docsUrl ? `${this.$root.docsUrl}/goto/ical-calendar` : null; }, + relationEditUrl() { + return `/manage/admin_unit/${this.$root.currentAdminUnit.id}/relations/${this.relation.id}/update`; + }, + relationCreateUrl() { + return `/manage/admin_unit/${this.$root.currentAdminUnit.id}/relations/create?target=${this.organizationId}&verify=1`; + }, }, mounted() { this.isLoading = false; + this.isLoadingRelation = false; this.organization = null; + this.relation = null; + this.canRelation = this.$root.has_access("admin_unit:update"); + this.relationDoesNotExist = false; this.loadData(); + this.loadRelationData(); }, methods: { loadData() { @@ -124,6 +194,33 @@ const OrganizationRead = { this.organization = response.data; }); }, + loadRelationData() { + if (!this.$root.hasOwnProperty("currentAdminUnit")) { + return; + } + const vm = this; + axios + .get(`/api/v1/organizations/${this.$root.currentAdminUnit.id}/relations/outgoing/${this.organizationId}`, { + withCredentials: true, + handleLoading: this.handleLoadingRelation, + handler: { + handleLoading: function(isLoading) { + vm.isLoadingRelation = isLoading; + }, + handleRequestError: function(error, message) { + const status = error && error.response && error.response.status; + if (status == 404) { + vm.relationDoesNotExist = true; + return; + } + this.$root.makeErrorToast(message); + } + } + }) + .then((response) => { + this.relation = response.data; + }); + }, handleLoading(isLoading) { this.isLoading = isLoading; }, diff --git a/project/templates/layout_vue.html b/project/templates/layout_vue.html index 8037b2d..369baad 100644 --- a/project/templates/layout_vue.html +++ b/project/templates/layout_vue.html @@ -348,6 +348,11 @@ axios.defaults.baseURL = "{{ get_base_url() }}"; axios.defaults.headers.common["X-CSRFToken"] = "{{ csrf_token() }}"; + + {% if current_admin_unit %} + axios.defaults.headers.common["X-OrganizationId"] = "{{ current_admin_unit.id }}"; + {% endif %} + axios.interceptors.request.use( function (config) { if (config) { @@ -381,6 +386,29 @@ vue_app_data["docsUrl"] = "{{ config["DOCS_URL"] }}"; {% endif %} + {% if current_user %} + vue_app_data["currentUser"] = { + {% if current_user.is_authenticated %} + isAuthenticated: true, + id: "{{ current_user.id }}", + email: "{{ current_user.email }}", + roles: [{% for role in get_current_user_roles() %}"{{ role }}"{% if not loop.last %},{% endif %}{% endfor %}], + permissions: [{% for permission in get_current_user_permissions() %}"{{ permission }}"{% if not loop.last %},{% endif %}{% endfor %}] + {% else %} + isAuthenticated: false + {% endif %} + }; + {% endif %} + + {% if current_admin_unit %} + vue_app_data["currentAdminUnit"] = { + id: "{{ current_admin_unit.id }}", + name: "{{ current_admin_unit.name }}", + roles: [{% for role in get_current_admin_unit_roles() %}"{{ role }}"{% if not loop.last %},{% endif %}{% endfor %}], + permissions: [{% for permission in get_current_admin_unit_permissions() %}"{{ permission }}"{% if not loop.last %},{% endif %}{% endfor %}] + }; + {% endif %} + {% block vue_init_data %} var vue_init_data = { el: "#vue-container", @@ -397,6 +425,14 @@ config.handler.handleRequestStart(); } + if ( + config && + config.handler && + config.handler.hasOwnProperty("handleLoading") + ) { + config.handler.handleLoading(true); + } + if ( config && config.hasOwnProperty("handleLoading") @@ -413,6 +449,14 @@ config.handler.handleRequestFinish(); } + if ( + config && + config.handler && + config.handler.hasOwnProperty("handleLoading") + ) { + config.handler.handleLoading(false); + } + if ( config && config.hasOwnProperty("handleLoading") @@ -479,6 +523,11 @@ }); }, goBack(fallbackPath) { + if ('referrer' in document) { + window.location = document.referrer; + return; + } + window.history.length > 1 ? this.$router.go(-1) : this.$router.push({ path: fallbackPath }) }, render_event_date_instance(value, allday, format = "dd. DD.MM.YYYY LT", alldayFormat = "dd. DD.MM.YYYY") { @@ -530,6 +579,22 @@ url_for_image(image, size) { return `${axios.defaults.baseURL}${image.image_url}?s=${size}` }, + has_access(permission) { + if (!this.currentUser.isAuthenticated) { + return false; + } + + if (this.currentUser.permissions.includes(permission)) { + return true; + } + + if (this.hasOwnProperty("currentAdminUnit") && + this.currentAdminUnit.permissions.includes(permission)) { + return true; + } + + return false; + } }, }; {% endblock %} diff --git a/tests/api/test_organization.py b/tests/api/test_organization.py index f478655..c9ce2c4 100644 --- a/tests/api/test_organization.py +++ b/tests/api/test_organization.py @@ -486,6 +486,26 @@ def test_references_outgoing(client, seeder: Seeder, utils: UtilActions): utils.get_json_ok(url) +def test_outgoing_relation_read(client, seeder: Seeder, utils: UtilActions): + user_id, admin_unit_id = seeder.setup_api_access() + ( + other_user_id, + other_admin_unit_id, + relation_id, + ) = seeder.create_any_admin_unit_relation(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_outgoing_relation", + id=admin_unit_id, + target_id=other_admin_unit_id, + ) + response = utils.get_json(url) + utils.assert_response_ok(response) + assert response.json["id"] == relation_id + assert response.json["source_organization"]["id"] == admin_unit_id + assert response.json["target_organization"]["id"] == other_admin_unit_id + + def test_outgoing_relation_list(client, seeder: Seeder, utils: UtilActions): user_id, admin_unit_id = seeder.setup_api_access() (