From 71028f57d11fadbd8ce8c5e7c8a159bb4021e228 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Thu, 14 Oct 2021 13:09:32 +0200 Subject: [PATCH] Organisationen einladen #307 --- cypress/integration/admin_unit.js | 29 +++- .../admin_unit_organization_invitation.js | 60 ++++++++ cypress/integration/manage.js | 24 ++- cypress/integration/user.js | 39 ++++- cypress/support/commands.js | 13 +- cypress/support/index.js | 11 +- messages.pot | 135 +++++++++++------ migrations/versions/920329927dc6_.py | 66 ++++++++ project/api/__init__.py | 4 + project/api/organization/resources.py | 60 ++++++++ project/api/organization/schemas.py | 2 + .../api/organization_invitation/__init__.py | 0 .../api/organization_invitation/resources.py | 94 ++++++++++++ .../api/organization_invitation/schemas.py | 83 ++++++++++ project/api/user/resources.py | 87 +++++++++++ project/cli/test.py | 27 ++++ project/forms/admin.py | 7 + project/i10n.py | 2 + project/models.py | 51 +++++++ project/services/admin_unit.py | 44 +++++- .../static/vue/common/validated-switch.vue.js | 55 +++++++ .../create.vue.js | 127 ++++++++++++++++ .../list.vue.js | 137 +++++++++++++++++ .../update.vue.js | 142 ++++++++++++++++++ .../vue/organization-relations/create.vue.js | 22 +-- .../vue/organization-relations/update.vue.js | 26 ++-- .../user-organization-invitations/read.vue.js | 84 +++++++++++ project/templates/_macros.html | 11 +- .../templates/admin/update_admin_unit.html | 1 + project/templates/admin_unit/create.html | 2 +- ...ganization_invitation_accepted_notice.html | 7 + ...rganization_invitation_accepted_notice.txt | 4 + .../email/organization_invitation_notice.html | 6 + .../email/organization_invitation_notice.txt | 3 + project/templates/layout.html | 3 + project/templates/layout_vue.html | 26 ++++ project/templates/manage/admin_units.html | 16 +- .../manage/organization_invitations.html | 34 +++++ project/templates/profile.html | 42 ------ .../user/organization_invitations.html | 22 +++ .../translations/de/LC_MESSAGES/messages.mo | Bin 31533 -> 32556 bytes .../translations/de/LC_MESSAGES/messages.po | 139 +++++++++++------ .../translations/en/LC_MESSAGES/messages.mo | Bin 3365 -> 3365 bytes .../translations/en/LC_MESSAGES/messages.po | 135 +++++++++++------ project/utils.py | 4 + project/views/admin_unit.py | 56 ++++++- project/views/admin_unit_member_invitation.py | 3 +- project/views/manage.py | 49 +++++- project/views/user.py | 32 +++- tests/api/test_organization.py | 54 +++++++ tests/api/test_organization_invitation.py | 87 +++++++++++ tests/api/test_user.py | 65 ++++++++ tests/seeder.py | 27 ++++ tests/test_models.py | 20 ++- tests/views/test_admin.py | 3 + tests/views/test_admin_unit.py | 71 +++++++++ tests/views/test_manage.py | 26 ++++ tests/views/test_user.py | 31 ++++ 58 files changed, 2181 insertions(+), 229 deletions(-) create mode 100644 cypress/integration/admin_unit_organization_invitation.js create mode 100644 migrations/versions/920329927dc6_.py create mode 100644 project/api/organization_invitation/__init__.py create mode 100644 project/api/organization_invitation/resources.py create mode 100644 project/api/organization_invitation/schemas.py create mode 100644 project/api/user/resources.py create mode 100644 project/static/vue/common/validated-switch.vue.js create mode 100644 project/static/vue/organization-organization-invitations/create.vue.js create mode 100644 project/static/vue/organization-organization-invitations/list.vue.js create mode 100644 project/static/vue/organization-organization-invitations/update.vue.js create mode 100644 project/static/vue/user-organization-invitations/read.vue.js create mode 100644 project/templates/email/organization_invitation_accepted_notice.html create mode 100644 project/templates/email/organization_invitation_accepted_notice.txt create mode 100644 project/templates/email/organization_invitation_notice.html create mode 100644 project/templates/email/organization_invitation_notice.txt create mode 100644 project/templates/manage/organization_invitations.html create mode 100644 project/templates/user/organization_invitations.html create mode 100644 tests/api/test_organization_invitation.py create mode 100644 tests/api/test_user.py diff --git a/cypress/integration/admin_unit.js b/cypress/integration/admin_unit.js index bab5955..a8d88cc 100644 --- a/cypress/integration/admin_unit.js +++ b/cypress/integration/admin_unit.js @@ -1,5 +1,5 @@ describe("Admin Unit", () => { - it("creates", () => { + it.skip("creates", () => { cy.login(); cy.visit("/admin_unit/create"); cy.get("#name").type("Second Crew"); @@ -10,7 +10,30 @@ describe("Admin Unit", () => { cy.url().should("include", "/manage/admin_unit/"); }); - it("updates", () => { + it("creates from invitation", () => { + cy.login(); + + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation( + adminUnitId, + "test@test.de" + ).then(function (invitationId) { + cy.visit("admin_unit/create?invitation_id=" + invitationId); + + cy.get("#name").should("have.value", "Invited Organization"); + cy.get("#short_name").should("have.value", "invitedorganization"); + cy.get("#short_name").should("have.class", "is-valid"); + + cy.get("#location-postalCode").type("38640"); + cy.get("#location-city").type("Goslar"); + cy.screenshot("create"); + cy.get("#submit").click(); + cy.url().should("include", "/manage/admin_unit/"); + }); + }); + }); + + it.skip("updates", () => { cy.login(); cy.createAdminUnit().then(function (adminUnitId) { cy.visit("/admin_unit/" + adminUnitId + "/update"); @@ -24,7 +47,7 @@ describe("Admin Unit", () => { }); }); - it("widgets", () => { + it.skip("widgets", () => { cy.login(); cy.createAdminUnit().then(function (adminUnitId) { cy.visit("/manage/admin_unit/" + adminUnitId + "/widgets"); diff --git a/cypress/integration/admin_unit_organization_invitation.js b/cypress/integration/admin_unit_organization_invitation.js new file mode 100644 index 0000000..0040dfb --- /dev/null +++ b/cypress/integration/admin_unit_organization_invitation.js @@ -0,0 +1,60 @@ +describe("Admin unit organization invitations", () => { + it("list", () => { + cy.login(); + cy.createAdminUnit().then(function (adminUnitId) { + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations"); + cy.screenshot("list"); + }); + }); + + it("create", () => { + cy.login(); + cy.createAdminUnit().then(function (adminUnitId) { + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations"); + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations/create"); + + cy.get('input[name=email]').type("invited@test.de"); + cy.get('input[name=organizationName]').type("Invited organization"); + cy.screenshot("create"); + cy.get("button[type=submit]").click(); + + cy.url().should( + "not.include", + "/create" + ); + + cy.get('button:contains(invited@test.de)'); + cy.screenshot("list-filled"); + }); + }); + + it("updates", () => { + cy.login(); + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation(adminUnitId).then(function (invitationId) { + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations"); + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations/" + invitationId + "/update"); + cy.screenshot("update"); + cy.get("button[type=submit]").click(); + cy.url().should( + "not.include", + "/update" + ); + }); + }); + }); + + it("deletes", () => { + cy.login(); + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation(adminUnitId).then(function (invitationId) { + cy.visit("/manage/admin_unit/" + adminUnitId + "/organization-invitations"); + + cy.get('.dropdown-toggle.btn-link').click(); + cy.get('.b-dropdown.show li:last').click(); + + cy.get('.dropdown-toggle.btn-link').should('not.exist'); + }); + }); + }); +}); diff --git a/cypress/integration/manage.js b/cypress/integration/manage.js index c372124..6eec169 100644 --- a/cypress/integration/manage.js +++ b/cypress/integration/manage.js @@ -1,5 +1,25 @@ describe("Manage", () => { - it("manage", () => { + it("Organizations", () => { + cy.login(); + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation( + adminUnitId, + "test@test.de" + ).then(function (organizationInvitationId) { + cy.createAdminUnitMemberInvitation(adminUnitId, "test@test.de").then(function ( + memberInvitationId + ) { + cy.visit("/manage/admin_units"); + cy.get("h1:contains('Einladungen')"); + cy.get("h1:contains('Organisationseinladungen')"); + cy.get("h1:contains('Organisationen')"); + cy.screenshot("organizations"); + }); + }); + }); + }); + + it.skip("Events", () => { cy.login(); cy.createAdminUnit().then(function (adminUnitId) { cy.createEvent(adminUnitId).then(function (eventId) { @@ -8,7 +28,7 @@ describe("Manage", () => { "include", "/manage/admin_unit/" + adminUnitId + "/events" ); - cy.screenshot("events") + cy.screenshot("events"); cy.get("#toggle-search-btn").click(); cy.screenshot("search-form"); diff --git a/cypress/integration/user.js b/cypress/integration/user.js index 64be2d3..92775f3 100644 --- a/cypress/integration/user.js +++ b/cypress/integration/user.js @@ -1,5 +1,5 @@ describe("User", () => { - it("registers user", () => { + it("registers user", () => { cy.visit("/register"); cy.screenshot("register"); @@ -51,7 +51,7 @@ describe("User", () => { cy.get("div.alert").should("contain", "Bestätigungsanleitung"); }); - it("login", () => { + it("login", () => { cy.visit("/login"); cy.screenshot("login"); @@ -85,4 +85,39 @@ describe("User", () => { cy.visit("/profile"); cy.screenshot("profile"); }); + + it("accepts organization invitation", () => { + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation(adminUnitId).then(function (invitationId) { + cy.createUser("invited@test.de").then(function () { + cy.login("invited@test.de", "password", "/user/organization-invitations/" + invitationId); + + cy.get("button.btn-success"); + cy.get(".b-overlay").should('not.exist'); + cy.screenshot("choice-accept"); + + cy.get("button.btn-success").click(); + cy.url().should("include", "admin_unit/create?invitation_id=" + invitationId); + }); + }); + }); + }); + + it("declines organization invitation", () => { + cy.createAdminUnit().then(function (adminUnitId) { + cy.createAdminUnitOrganizationInvitation(adminUnitId).then(function (invitationId) { + cy.createUser("invited@test.de").then(function () { + cy.login("invited@test.de", "password", "/user/organization-invitations/" + invitationId); + + cy.get("button.btn-danger"); + cy.get(".b-overlay").should('not.exist'); + cy.screenshot("choice-decline"); + + cy.get("button.btn-danger").click(); + cy.url().should("include", "manage/admin_units"); + cy.get("h1:contains('Organisationseinladungen')").should('not.exist'); + }); + }); + }); + }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index db4c1b7..9f3f666 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -141,6 +141,15 @@ Cypress.Commands.add("createAdminUnitRelation", (adminUnitId) => { }); }); +Cypress.Commands.add("createAdminUnitOrganizationInvitation", (adminUnitId, email="invited@test.de") => { + return cy + .logexec("flask test admin-unit-organization-invitation-create " + adminUnitId + ' "' + email + '"') + .then(function (result) { + let json = JSON.parse(result.stdout); + return json.invitation_id; + }); +}); + Cypress.Commands.add("createSuggestion", (adminUnitId) => { return cy .logexec("flask test suggestion-create " + adminUnitId) @@ -166,12 +175,12 @@ Cypress.Commands.add("assertRequired", (fieldId) => { Cypress.Commands.add( "login", - (email = "test@test.de", password = "password") => { + (email = "test@test.de", password = "password", redirectUrl = "/manage") => { cy.visit("/login"); cy.get("#email").type(email); cy.get("#password").type(password); cy.get("#submit").click(); - cy.url().should("include", "/manage"); + cy.url().should("include", redirectUrl); cy.getCookie("session").should("exist"); } ); diff --git a/cypress/support/index.js b/cypress/support/index.js index 2b371bd..26bb86c 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,8 +1,17 @@ import "./commands"; -import failOnConsoleError from 'cypress-fail-on-console-error'; +import failOnConsoleError from "cypress-fail-on-console-error"; failOnConsoleError(); +before(() => { + if (Cypress.browser.family === "chromium") { + Cypress.automation("remote:debugger:protocol", { + command: "Network.setCacheDisabled", + params: { cacheDisabled: true }, + }); + } +}); + beforeEach(() => { cy.setup(); }); diff --git a/messages.pot b/messages.pot index 54a6f77..e4b2be0 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: 2021-09-29 23:02+0200\n" +"POT-Creation-Date: 2021-10-14 10:41+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -146,34 +146,42 @@ msgid "Scope_profile" msgstr "" #: project/i10n.py:45 -msgid "Scope_organizer:write" +msgid "Scope_user:read" msgstr "" #: project/i10n.py:46 -msgid "Scope_place:write" +msgid "Scope_user:write" msgstr "" #: project/i10n.py:47 -msgid "Scope_event:write" +msgid "Scope_organizer:write" msgstr "" #: project/i10n.py:48 -msgid "Scope_organization:read" +msgid "Scope_place:write" msgstr "" #: project/i10n.py:49 -msgid "Scope_organization:write" +msgid "Scope_event:write" msgstr "" #: project/i10n.py:50 +msgid "Scope_organization:read" +msgstr "" + +#: project/i10n.py:51 +msgid "Scope_organization:write" +msgstr "" + +#: project/i10n.py:52 msgid "There must be no self-reference." msgstr "" -#: project/utils.py:11 +#: project/utils.py:15 msgid "Event_" msgstr "" -#: project/utils.py:15 +#: project/utils.py:19 msgid "." msgstr "" @@ -181,24 +189,29 @@ msgstr "" msgid "message" msgstr "" -#: project/forms/admin.py:10 project/templates/layout.html:305 +#: project/api/organization/resources.py:348 +#: project/views/admin_unit_member_invitation.py:92 +msgid "You have received an invitation" +msgstr "" + +#: project/forms/admin.py:10 project/templates/layout.html:308 #: project/views/root.py:42 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:309 +#: project/forms/admin.py:11 project/templates/layout.html:312 #: project/views/root.py:50 msgid "Legal notice" msgstr "" #: project/forms/admin.py:12 project/templates/_macros.html:1409 -#: project/templates/layout.html:313 +#: project/templates/layout.html:316 #: project/templates/widget/event_suggestion/create.html:204 -#: project/views/admin_unit.py:36 project/views/root.py:58 +#: project/views/admin_unit.py:50 project/views/root.py:58 msgid "Contact" msgstr "" -#: project/forms/admin.py:13 project/templates/layout.html:317 +#: project/forms/admin.py:13 project/templates/layout.html:320 #: project/views/root.py:66 msgid "Privacy" msgstr "" @@ -208,7 +221,7 @@ msgid "Save" msgstr "" #: project/forms/admin.py:19 project/forms/admin_unit_member.py:12 -#: project/forms/admin_unit_member.py:32 project/templates/profile.html:66 +#: project/forms/admin_unit_member.py:32 msgid "Roles" msgstr "" @@ -244,14 +257,22 @@ msgid "If set, members of the organization can create other organizations." msgstr "" #: project/forms/admin.py:44 -msgid "Verify other organizations" +msgid "Invite other organizations" msgstr "" #: project/forms/admin.py:45 +msgid "If set, members of the organization can invite other organizations." +msgstr "" + +#: project/forms/admin.py:51 +msgid "Verify other organizations" +msgstr "" + +#: project/forms/admin.py:52 msgid "If set, members of the organization can verify other organizations." msgstr "" -#: project/forms/admin.py:50 project/templates/admin/update_admin_unit.html:4 +#: project/forms/admin.py:57 project/templates/admin/update_admin_unit.html:4 #: project/templates/admin/update_admin_unit.html:8 msgid "Update organization" msgstr "" @@ -295,7 +316,6 @@ msgstr "" #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/oauth2_client/list.html:25 -#: project/templates/profile.html:45 project/templates/profile.html:65 msgid "Name" msgstr "" @@ -342,7 +362,7 @@ msgstr "" #: project/forms/admin_unit.py:63 project/templates/admin_unit/create.html:5 #: project/templates/admin_unit/create.html:22 -#: project/templates/manage/admin_units.html:18 +#: project/templates/manage/admin_units.html:27 msgid "Create organization" msgstr "" @@ -876,7 +896,7 @@ msgid "Distance" msgstr "" #: project/forms/event_date.py:32 project/forms/planing.py:36 -#: project/templates/widget/event_date/list.html:60 +#: project/templates/widget/event_date/list.html:61 msgid "Find" msgstr "" @@ -1324,8 +1344,7 @@ msgstr "" #: project/templates/admin/admin_units.html:11 #: project/templates/layout.html:186 #: project/templates/manage/admin_units.html:3 -#: project/templates/manage/admin_units.html:16 -#: project/templates/profile.html:60 +#: project/templates/manage/admin_units.html:25 msgid "Organizations" msgstr "" @@ -1409,27 +1428,34 @@ msgstr "" msgid "Relations" msgstr "" +#: project/templates/layout.html:267 +#: project/templates/manage/admin_units.html:17 +#: project/templates/manage/organization_invitations.html:4 +#: project/templates/user/organization_invitations.html:4 +msgid "Organization invitations" +msgstr "" + #: project/templates/admin/admin.html:15 #: project/templates/admin/settings.html:4 #: project/templates/admin/settings.html:8 #: project/templates/admin_unit/update.html:6 #: project/templates/admin_unit/update.html:23 -#: project/templates/layout.html:266 project/templates/manage/widgets.html:12 +#: project/templates/layout.html:269 project/templates/manage/widgets.html:12 #: project/templates/profile.html:19 msgid "Settings" msgstr "" -#: project/templates/layout.html:267 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:270 project/templates/manage/reviews.html:10 #: project/templates/manage/widgets.html:5 #: project/templates/manage/widgets.html:9 msgid "Widgets" msgstr "" -#: project/templates/layout.html:277 +#: project/templates/layout.html:280 msgid "Switch organization" msgstr "" -#: project/templates/developer/read.html:4 project/templates/layout.html:327 +#: project/templates/developer/read.html:4 project/templates/layout.html:330 #: project/templates/profile.html:29 msgid "Developer" msgstr "" @@ -1445,11 +1471,6 @@ msgstr "" msgid "OAuth2 clients" msgstr "" -#: project/templates/manage/admin_units.html:8 -#: project/templates/manage/members.html:9 project/templates/profile.html:40 -msgid "Invitations" -msgstr "" - #: project/templates/admin/admin.html:23 project/templates/admin/users.html:4 #: project/templates/admin/users.html:11 msgid "Users" @@ -1497,6 +1518,7 @@ msgid "You have been invited to join %(admin_unit_name)s." msgstr "" #: project/templates/email/invitation_notice.html:5 +#: project/templates/email/organization_invitation_notice.html:5 msgid "Click here to view the invitation" msgstr "" @@ -1508,6 +1530,22 @@ msgstr "" msgid "this is a message from Oveda - Die offene Veranstaltungsdatenbank." msgstr "" +#: project/templates/email/organization_invitation_accepted_notice.html:4 +#, python-format +msgid "" +"%(email)s accepted your invitation to create the organisation " +"%(admin_unit_name)s." +msgstr "" + +#: project/templates/email/organization_invitation_accepted_notice.html:6 +msgid "Click here to view the relation" +msgstr "" + +#: project/templates/email/organization_invitation_notice.html:4 +#, python-format +msgid "%(admin_unit_name)s invited you to create an organization." +msgstr "" + #: project/templates/email/reference_auto_verified_notice.html:4 msgid "There is a new referenced event that was automatically verified." msgstr "" @@ -1675,6 +1713,15 @@ msgstr "" msgid "Would you like to accept the invitation from %(name)s?" msgstr "" +#: project/templates/manage/admin_units.html:8 +#: project/templates/manage/members.html:9 +msgid "Invitations" +msgstr "" + +#: project/templates/manage/admin_units.html:29 +msgid "Invite organization" +msgstr "" + #: project/templates/manage/delete_member.html:13 msgid "Member" msgstr "" @@ -1835,7 +1882,7 @@ msgstr "" msgid "Widget" msgstr "" -#: project/templates/widget/event_date/list.html:123 +#: project/templates/widget/event_date/list.html:124 msgid "Print" msgstr "" @@ -1855,7 +1902,7 @@ msgstr "" msgid "Organization successfully updated" msgstr "" -#: project/views/admin.py:68 project/views/manage.py:274 +#: project/views/admin.py:68 project/views/manage.py:317 msgid "Settings successfully updated" msgstr "" @@ -1863,20 +1910,24 @@ msgstr "" msgid "User successfully updated" msgstr "" -#: project/views/admin_unit.py:32 +#: project/views/admin_unit.py:46 msgid "" "Organizations cannot currently be created. The project is in a closed " "test phase. If you are interested, you can contact us." msgstr "" -#: project/views/admin_unit.py:50 +#: project/views/admin_unit.py:79 msgid "Organization successfully created" msgstr "" -#: project/views/admin_unit.py:76 +#: project/views/admin_unit.py:105 msgid "AdminUnit successfully updated" msgstr "" +#: project/views/admin_unit.py:127 +msgid "Organization invitation accepted" +msgstr "" + #: project/views/admin_unit_member.py:43 msgid "Member successfully updated" msgstr "" @@ -1889,27 +1940,23 @@ msgstr "" msgid "Member successfully deleted" msgstr "" -#: project/views/admin_unit_member_invitation.py:44 +#: project/views/admin_unit_member_invitation.py:45 msgid "Invitation successfully accepted" msgstr "" -#: project/views/admin_unit_member_invitation.py:51 +#: project/views/admin_unit_member_invitation.py:52 msgid "Invitation successfully declined" msgstr "" -#: project/views/admin_unit_member_invitation.py:91 -msgid "You have received an invitation" -msgstr "" - -#: project/views/admin_unit_member_invitation.py:96 +#: project/views/admin_unit_member_invitation.py:97 msgid "Invitation successfully sent" msgstr "" -#: project/views/admin_unit_member_invitation.py:119 +#: project/views/admin_unit_member_invitation.py:120 msgid "Entered email does not match invitation email" msgstr "" -#: project/views/admin_unit_member_invitation.py:124 +#: project/views/admin_unit_member_invitation.py:125 msgid "Invitation successfully deleted" msgstr "" diff --git a/migrations/versions/920329927dc6_.py b/migrations/versions/920329927dc6_.py new file mode 100644 index 0000000..85592fb --- /dev/null +++ b/migrations/versions/920329927dc6_.py @@ -0,0 +1,66 @@ +"""empty message + +Revision ID: 920329927dc6 +Revises: fca98f434287 +Create Date: 2021-10-03 22:32:34.070105 + +""" +import sqlalchemy as sa +import sqlalchemy_utils +from alembic import op + +from project import dbtypes + +# revision identifiers, used by Alembic. +revision = "920329927dc6" +down_revision = "fca98f434287" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "adminunitinvitation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("admin_unit_id", sa.Integer(), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("admin_unit_name", sa.String(length=255), nullable=True), + sa.Column( + "relation_auto_verify_event_reference_requests", + sa.Boolean(), + server_default="0", + nullable=False, + ), + sa.Column("relation_verify", sa.Boolean(), server_default="0", nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("created_by_id", sa.Integer(), nullable=True), + sa.Column("updated_by_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["admin_unit_id"], + ["adminunit.id"], + ), + sa.ForeignKeyConstraint( + ["created_by_id"], + ["user.id"], + ), + sa.ForeignKeyConstraint( + ["updated_by_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.add_column( + "adminunit", + sa.Column("can_invite_other", sa.Boolean(), server_default="0", nullable=False), + ) + op.add_column( + "adminunitrelation", + sa.Column("invited", sa.Boolean(), server_default="0", nullable=False), + ) + + +def downgrade(): + op.drop_column("adminunit", "can_invite_other") + op.drop_column("adminunitrelation", "invited") + op.drop_table("adminunitinvitation") diff --git a/project/api/__init__.py b/project/api/__init__.py index 2b7d2c3..f85e194 100644 --- a/project/api/__init__.py +++ b/project/api/__init__.py @@ -117,6 +117,8 @@ class RestApi(Api): scope_list = [ "openid", "profile", + "user:read", + "user:write", "organizer:write", "place:write", "event:write", @@ -191,6 +193,8 @@ import project.api.event_category.resources import project.api.event_date.resources import project.api.event_reference.resources import project.api.organization.resources +import project.api.organization_invitation.resources import project.api.organization_relation.resources import project.api.organizer.resources import project.api.place.resources +import project.api.user.resources diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 6f2aa98..6230198 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -1,4 +1,5 @@ from flask_apispec import doc, marshal_with, use_kwargs +from flask_babelex import gettext from sqlalchemy import and_ from project import db @@ -30,6 +31,12 @@ from project.api.organization.schemas import ( OrganizationListResponseSchema, OrganizationSchema, ) +from project.api.organization_invitation.schemas import ( + OrganizationInvitationCreateRequestSchema, + OrganizationInvitationIdSchema, + OrganizationInvitationListRequestSchema, + OrganizationInvitationListResponseSchema, +) from project.api.organization_relation.schemas import ( OrganizationRelationCreateRequestSchema, OrganizationRelationIdSchema, @@ -52,6 +59,7 @@ from project.api.resources import BaseResource, require_api_access from project.models import AdminUnit, Event, PublicStatus from project.oauth2 import require_oauth from project.services.admin_unit import ( + get_admin_unit_invitation_query, get_admin_unit_query, get_organizer_query, get_place_query, @@ -63,6 +71,7 @@ from project.services.reference import ( get_reference_outgoing_query, get_relation_outgoing_query, ) +from project.views.utils import send_mail class OrganizationResource(BaseResource): @@ -298,6 +307,52 @@ class OrganizationOutgoingRelationListResource(BaseResource): return relation, 201 +class OrganizationOrganizationInvitationListResource(BaseResource): + @doc( + summary="List organization invitations of organization", + tags=["Organizations", "Organization Invitations"], + security=[{"oauth2": ["organization:read"]}], + ) + @use_kwargs(OrganizationInvitationListRequestSchema, location=("query")) + @marshal_with(OrganizationInvitationListResponseSchema) + @require_api_access("organization:read") + def get(self, id, **kwargs): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + access_or_401(admin_unit, "admin_unit:update") + + pagination = get_admin_unit_invitation_query(admin_unit).paginate() + return pagination + + @doc( + summary="Add new organization invitation", + tags=["Organizations", "Organization Invitations"], + security=[{"oauth2": ["organization:write"]}], + ) + @use_kwargs(OrganizationInvitationCreateRequestSchema, location="json", apply=False) + @marshal_with(OrganizationInvitationIdSchema, 201) + @require_api_access("organization: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, "admin_unit:update") + + invitation = self.create_instance( + OrganizationInvitationCreateRequestSchema, admin_unit_id=admin_unit.id + ) + db.session.add(invitation) + db.session.commit() + + send_mail( + invitation.email, + gettext("You have received an invitation"), + "organization_invitation_notice", + invitation=invitation, + ) + + return invitation, 201 + + add_api_resource(OrganizationResource, "/organizations/", "api_v1_organization") add_api_resource( OrganizationEventDateSearchResource, @@ -340,3 +395,8 @@ add_api_resource( "/organizations//relations/outgoing", "api_v1_organization_outgoing_relation_list", ) +add_api_resource( + OrganizationOrganizationInvitationListResource, + "/organizations//organization-invitations", + "api_v1_organization_organization_invitation_list", +) diff --git a/project/api/organization/schemas.py b/project/api/organization/schemas.py index f54937a..4d3a7f2 100644 --- a/project/api/organization/schemas.py +++ b/project/api/organization/schemas.py @@ -43,12 +43,14 @@ class OrganizationSchema(OrganizationBaseSchema): location = fields.Nested(LocationSchema) logo = fields.Nested(ImageSchema) can_verify_other = marshmallow.auto_field() + incoming_reference_requests_allowed = marshmallow.auto_field() @post_dump(pass_original=True) def remove_private_fields(self, data, original_data, **kwargs): login_api_user() if not has_access(original_data, "admin_unit:update"): data.pop("can_verify_other", None) + data.pop("incoming_reference_requests_allowed", None) return data diff --git a/project/api/organization_invitation/__init__.py b/project/api/organization_invitation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/api/organization_invitation/resources.py b/project/api/organization_invitation/resources.py new file mode 100644 index 0000000..d7fe511 --- /dev/null +++ b/project/api/organization_invitation/resources.py @@ -0,0 +1,94 @@ +from flask.helpers import make_response +from flask_apispec import doc, marshal_with +from flask_apispec.annotations import use_kwargs + +from project import db +from project.access import access_or_401, login_api_user_or_401 +from project.api import add_api_resource +from project.api.organization_invitation.schemas import ( + OrganizationInvitationPatchRequestSchema, + OrganizationInvitationSchema, + OrganizationInvitationUpdateRequestSchema, +) +from project.api.resources import BaseResource, require_api_access +from project.models import AdminUnitInvitation + + +class OrganizationInvitationResource(BaseResource): + @doc( + summary="Get organization invitation", + tags=["Organization Invitations"], + security=[{"oauth2": ["organization:read"]}], + ) + @marshal_with(OrganizationInvitationSchema) + @require_api_access("organization:read") + def get(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + access_or_401(invitation.adminunit, "admin_unit:update") + + return invitation + + @doc( + summary="Update organization invitation", + tags=["Organization Invitations"], + security=[{"oauth2": ["organization:write"]}], + ) + @use_kwargs(OrganizationInvitationUpdateRequestSchema, location="json", apply=False) + @marshal_with(None, 204) + @require_api_access("organization:write") + def put(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + access_or_401(invitation.adminunit, "admin_unit:update") + + invitation = self.update_instance( + OrganizationInvitationUpdateRequestSchema, instance=invitation + ) + db.session.commit() + + return make_response("", 204) + + @doc( + summary="Patch organization invitation", + tags=["Organization Invitations"], + security=[{"oauth2": ["organization:write"]}], + ) + @use_kwargs(OrganizationInvitationPatchRequestSchema, location="json", apply=False) + @marshal_with(None, 204) + @require_api_access("organization:write") + def patch(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + access_or_401(invitation.adminunit, "admin_unit:update") + + invitation = self.update_instance( + OrganizationInvitationPatchRequestSchema, instance=invitation + ) + db.session.commit() + + return make_response("", 204) + + @doc( + summary="Delete organization invitation", + tags=["Organization Invitations"], + security=[{"oauth2": ["organization:write"]}], + ) + @marshal_with(None, 204) + @require_api_access("organization:write") + def delete(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + access_or_401(invitation.adminunit, "admin_unit:update") + + db.session.delete(invitation) + db.session.commit() + + return make_response("", 204) + + +add_api_resource( + OrganizationInvitationResource, + "/organization-invitation/", + "api_v1_organization_invitation", +) diff --git a/project/api/organization_invitation/schemas.py b/project/api/organization_invitation/schemas.py new file mode 100644 index 0000000..06d41b7 --- /dev/null +++ b/project/api/organization_invitation/schemas.py @@ -0,0 +1,83 @@ +from marshmallow import fields + +from project.api import marshmallow +from project.api.organization.schemas import OrganizationRefSchema +from project.api.schemas import ( + IdSchemaMixin, + PaginationRequestSchema, + PaginationResponseSchema, + SQLAlchemyBaseSchema, + TrackableSchemaMixin, +) +from project.models import AdminUnitInvitation + + +class OrganizationInvitationModelSchema(SQLAlchemyBaseSchema): + class Meta: + model = AdminUnitInvitation + load_instance = True + + +class OrganizationInvitationIdSchema(OrganizationInvitationModelSchema, IdSchemaMixin): + pass + + +class OrganizationInvitationBaseSchemaMixin(TrackableSchemaMixin): + organization_name = fields.Str(attribute="admin_unit_name") + relation_auto_verify_event_reference_requests = marshmallow.auto_field() + relation_verify = marshmallow.auto_field() + + +class OrganizationInvitationSchema( + OrganizationInvitationIdSchema, OrganizationInvitationBaseSchemaMixin +): + email = marshmallow.auto_field() + organization = fields.Nested(OrganizationRefSchema, attribute="adminunit") + + +class OrganizationInvitationRefSchema(OrganizationInvitationIdSchema): + organization_name = fields.Str(attribute="admin_unit_name") + email = marshmallow.auto_field() + + +class OrganizationInvitationListRequestSchema(PaginationRequestSchema): + pass + + +class OrganizationInvitationListResponseSchema(PaginationResponseSchema): + items = fields.List( + fields.Nested(OrganizationInvitationRefSchema), + metadata={"description": "Organization invitations"}, + ) + + +class OrganizationInvitationWriteSchemaMixin(object): + email = marshmallow.auto_field() + + +class OrganizationInvitationCreateRequestSchema( + OrganizationInvitationModelSchema, + OrganizationInvitationBaseSchemaMixin, + OrganizationInvitationWriteSchemaMixin, +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + +class OrganizationInvitationUpdateRequestSchema( + OrganizationInvitationModelSchema, + OrganizationInvitationBaseSchemaMixin, +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + +class OrganizationInvitationPatchRequestSchema( + OrganizationInvitationModelSchema, + OrganizationInvitationBaseSchemaMixin, +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_patch_schema() diff --git a/project/api/user/resources.py b/project/api/user/resources.py new file mode 100644 index 0000000..7be80ae --- /dev/null +++ b/project/api/user/resources.py @@ -0,0 +1,87 @@ +from flask import abort +from flask.helpers import make_response +from flask_apispec import doc, marshal_with, use_kwargs +from flask_security import current_user + +from project import db +from project.access import login_api_user_or_401 +from project.api import add_api_resource +from project.api.organization_invitation.schemas import ( + OrganizationInvitationListRequestSchema, + OrganizationInvitationListResponseSchema, + OrganizationInvitationSchema, +) +from project.api.resources import BaseResource, require_api_access +from project.models import AdminUnitInvitation +from project.services.admin_unit import get_admin_unit_organization_invitations_query +from project.utils import strings_are_equal_ignoring_case + + +def invitation_receiver_or_401(invitation: AdminUnitInvitation): + if not strings_are_equal_ignoring_case(invitation.email, current_user.email): + abort(401) + + +class UserOrganizationInvitationListResource(BaseResource): + @doc( + summary="List organization invitations of user", + tags=["Users", "Organization Invitations"], + security=[{"oauth2": ["user:read"]}], + ) + @use_kwargs(OrganizationInvitationListRequestSchema, location=("query")) + @marshal_with(OrganizationInvitationListResponseSchema) + @require_api_access("user:read") + def get(self, **kwargs): + login_api_user_or_401() + + pagination = get_admin_unit_organization_invitations_query( + current_user.email + ).paginate() + + return pagination + + +class UserOrganizationInvitationResource(BaseResource): + @doc( + summary="Get organization invitation of user", + tags=["Users", "Organization Invitations"], + security=[{"oauth2": ["user:read"]}], + ) + @marshal_with(OrganizationInvitationSchema) + @require_api_access("user:read") + def get(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + invitation_receiver_or_401(invitation) + + return invitation + + @doc( + summary="Delete organization invitation of user", + tags=["Users", "Organization Invitations"], + security=[{"oauth2": ["user:write"]}], + ) + @marshal_with(None, 204) + @require_api_access("user:write") + def delete(self, id): + login_api_user_or_401() + invitation = AdminUnitInvitation.query.get_or_404(id) + invitation_receiver_or_401(invitation) + + db.session.delete(invitation) + db.session.commit() + + return make_response("", 204) + + +add_api_resource( + UserOrganizationInvitationListResource, + "/user/organization-invitations", + "api_v1_user_organization_invitation_list", +) + +add_api_resource( + UserOrganizationInvitationResource, + "/user/organization-invitation/", + "api_v1_user_organization_invitation", +) diff --git a/project/cli/test.py b/project/cli/test.py index e1e96f6..6f7e088 100644 --- a/project/cli/test.py +++ b/project/cli/test.py @@ -11,6 +11,7 @@ from project.api import scope_list from project.init_data import create_initial_data from project.models import ( AdminUnit, + AdminUnitInvitation, Event, EventAttendanceMode, EventReference, @@ -137,6 +138,7 @@ def _create_admin_unit(user_id, name, verified=False): admin_unit.suggestions_enabled = True admin_unit.can_create_other = True admin_unit.can_verify_other = True + admin_unit.can_invite_other = True admin_unit.location = Location() admin_unit.location.postalCode = "38640" admin_unit.location.city = "Goslar" @@ -376,6 +378,31 @@ def create_admin_unit_relation(admin_unit_id): click.echo(json.dumps(result)) +def _create_admin_unit_invitation( + admin_unit_id, + email="invited@test.de", + admin_unit_name="Invited Organization", +): + invitation = AdminUnitInvitation() + invitation.admin_unit_id = admin_unit_id + invitation.email = email + invitation.admin_unit_name = admin_unit_name + db.session.add(invitation) + db.session.commit() + return invitation.id + + +@test_cli.command("admin-unit-organization-invitation-create") +@click.argument("admin_unit_id") +@click.argument("email") +def create_admin_unit_organization_invitation(admin_unit_id, email): + invitation_id = _create_admin_unit_invitation(admin_unit_id, email) + result = { + "invitation_id": invitation_id, + } + click.echo(json.dumps(result)) + + def _create_event_suggestion(admin_unit_id, free_text=False): suggestion = EventSuggestion() suggestion.admin_unit_id = admin_unit_id diff --git a/project/forms/admin.py b/project/forms/admin.py index 49e54bd..8842681 100644 --- a/project/forms/admin.py +++ b/project/forms/admin.py @@ -40,6 +40,13 @@ class UpdateAdminUnitForm(FlaskForm): ), validators=[Optional()], ) + can_invite_other = BooleanField( + lazy_gettext("Invite other organizations"), + description=lazy_gettext( + "If set, members of the organization can invite other organizations." + ), + validators=[Optional()], + ) can_verify_other = BooleanField( lazy_gettext("Verify other organizations"), description=lazy_gettext( diff --git a/project/i10n.py b/project/i10n.py index f868ccf..0ad2c07 100644 --- a/project/i10n.py +++ b/project/i10n.py @@ -42,6 +42,8 @@ def print_dynamic_texts(): gettext("EventReviewStatus.rejected") gettext("Scope_openid") gettext("Scope_profile") + gettext("Scope_user:read") + gettext("Scope_user:write") gettext("Scope_organizer:write") gettext("Scope_place:write") gettext("Scope_event:write") diff --git a/project/models.py b/project/models.py index 382c3a2..6a2c1fd 100644 --- a/project/models.py +++ b/project/models.py @@ -296,6 +296,26 @@ class AdminUnitMemberInvitation(db.Model): roles = Column(UnicodeText()) +class AdminUnitInvitation(db.Model, TrackableMixin): + __tablename__ = "adminunitinvitation" + id = Column(Integer(), primary_key=True) + admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False) + email = Column(String(255), nullable=False) + admin_unit_name = Column(String(255)) + relation_auto_verify_event_reference_requests = Column( + Boolean(), + nullable=False, + default=False, + server_default="0", + ) + relation_verify = Column( + Boolean(), + nullable=False, + default=False, + server_default="0", + ) + + class AdminUnitRelation(db.Model, TrackableMixin): __tablename__ = "adminunitrelation" __table_args__ = ( @@ -325,6 +345,14 @@ class AdminUnitRelation(db.Model, TrackableMixin): server_default="0", ) ) + invited = deferred( + Column( + Boolean(), + nullable=False, + default=False, + server_default="0", + ) + ) def validate(self): source_id = ( @@ -362,6 +390,11 @@ class AdminUnit(db.Model, TrackableMixin): cascade="all, delete-orphan", backref=backref("adminunit", lazy=True), ) + admin_unit_invitations = relationship( + "AdminUnitInvitation", + cascade="all, delete-orphan", + backref=backref("adminunit", lazy=True), + ) events = relationship( "Event", cascade="all, delete-orphan", backref=backref("admin_unit", lazy=True) ) @@ -431,6 +464,14 @@ class AdminUnit(db.Model, TrackableMixin): server_default="0", ) ) + can_invite_other = deferred( + Column( + Boolean(), + nullable=False, + default=False, + server_default="0", + ) + ) outgoing_relations = relationship( "AdminUnitRelation", primaryjoin=remote(AdminUnitRelation.source_admin_unit_id) == id, @@ -496,6 +537,16 @@ def before_saving_admin_unit(mapper, connect, self): self.purge() +@listens_for(AdminUnit.can_invite_other, "set") +def set_admin_unit_can_invite_other(target, value, oldvalue, initiator): + if ( + not value + and target.admin_unit_invitations + and len(target.admin_unit_invitations) > 0 + ): + target.admin_unit_invitations = [] + + # Universal Types diff --git a/project/services/admin_unit.py b/project/services/admin_unit.py index 87127cc..645d4a5 100644 --- a/project/services/admin_unit.py +++ b/project/services/admin_unit.py @@ -3,6 +3,7 @@ from sqlalchemy import and_, func, or_ from project import db from project.models import ( AdminUnit, + AdminUnitInvitation, AdminUnitMember, AdminUnitMemberInvitation, AdminUnitMemberRole, @@ -13,9 +14,10 @@ from project.models import ( ) from project.services.image import upsert_image_with_data from project.services.location import assign_location_values +from project.utils import strings_are_equal_ignoring_case -def insert_admin_unit_for_user(admin_unit, user): +def insert_admin_unit_for_user(admin_unit, user, invitation=None): db.session.add(admin_unit) # Nutzer als Admin hinzufügen @@ -41,6 +43,7 @@ def insert_admin_unit_for_user(admin_unit, user): db.session.add(organizer) # Place anlegen + place = None if admin_unit.location: place = EventPlace() place.admin_unit_id = admin_unit.id @@ -49,8 +52,31 @@ def insert_admin_unit_for_user(admin_unit, user): assign_location_values(place.location, admin_unit.location) db.session.add(place) + # Beziehung anlegen + relation = None + if invitation: + inviting_admin_unit = get_admin_unit_by_id(invitation.admin_unit_id) + relation = upsert_admin_unit_relation(invitation.admin_unit_id, admin_unit.id) + relation.invited = True + + name_equals_suggested_name = strings_are_equal_ignoring_case( + admin_unit.name, invitation.admin_unit_name + ) + relation.auto_verify_event_reference_requests = ( + inviting_admin_unit.incoming_reference_requests_allowed + and invitation.relation_auto_verify_event_reference_requests + and name_equals_suggested_name + ) + relation.verify = ( + inviting_admin_unit.can_verify_other + and invitation.relation_verify + and name_equals_suggested_name + ) + db.session.commit() + return (organizer, place, relation) + def get_admin_unit_by_id(id): return AdminUnit.query.filter_by(id=id).first() @@ -204,3 +230,19 @@ def upsert_admin_unit_relation(source_admin_unit_id: int, target_admin_unit_id: result = insert_admin_unit_relation(source_admin_unit_id, target_admin_unit_id) return result + + +def get_admin_unit_invitation_query(admin_unit): + return AdminUnitInvitation.query.filter( + AdminUnitInvitation.admin_unit_id == admin_unit.id + ) + + +def get_admin_unit_organization_invitations_query(email): + return AdminUnitInvitation.query.filter( + func.lower(AdminUnitInvitation.email) == func.lower(email) + ) + + +def get_admin_unit_organization_invitations(email): + return get_admin_unit_organization_invitations_query(email).all() diff --git a/project/static/vue/common/validated-switch.vue.js b/project/static/vue/common/validated-switch.vue.js new file mode 100644 index 0000000..ed8d1af --- /dev/null +++ b/project/static/vue/common/validated-switch.vue.js @@ -0,0 +1,55 @@ +const ValidatedSwitch = { + template: ` +
+ + + + {{ $attrs.label }} + + + {{ validationContext.errors[0] }} + + + +
+ `, + props: { + vid: { + type: String + }, + rules: { + type: [Object, String], + default: "" + }, + value: { + type: null + } + }, + data: () => ({ + innerValue: "" + }), + watch: { + // Handles internal model changes. + innerValue(newVal) { + this.$emit("input", newVal); + }, + // Handles external model changes. + value(newVal) { + this.innerValue = newVal; + } + }, + created() { + if (this.value) { + this.innerValue = this.value; + } + }, + methods: { + getValidationState({ dirty, validated, valid = null }) { + return (this.rules != "" && (dirty || validated)) ? valid : null; + }, + }, +}; diff --git a/project/static/vue/organization-organization-invitations/create.vue.js b/project/static/vue/organization-organization-invitations/create.vue.js new file mode 100644 index 0000000..dde6129 --- /dev/null +++ b/project/static/vue/organization-organization-invitations/create.vue.js @@ -0,0 +1,127 @@ +const OrganizationOrganizationInvitationCreate = { + template: ` +
+

{{ $t("comp.title") }}

+ +
+ + + + + + + {{ $t("shared.cancel") }} + + + {{ $t("shared.submit") }} + + + +
+
+
+ `, + i18n: { + messages: { + en: { + comp: { + title: "Add invitation", + successMessage: "Invitation successfully created", + }, + }, + de: { + comp: { + title: "Einladung hinzufügen", + successMessage: "Einladung erfolgreich erstellt", + }, + }, + }, + }, + data: () => ({ + isLoadingAdminUnit: false, + isSubmitting: false, + adminUnit: null, + form: { + email: null, + organization_name: null, + relation_auto_verify_event_reference_requests: false, + relation_verify: true, + }, + }), + computed: { + adminUnitId() { + return this.$route.params.admin_unit_id + }, + }, + mounted() { + this.isLoadingAdminUnit = false; + this.adminUnit = null; + this.form = { + email: null, + organization_name: null, + relation_auto_verify_event_reference_requests: false, + relation_verify: true, + } + this.loadAdminUnit(); + }, + methods: { + loadAdminUnit() { + axios + .get(`/api/v1/organizations/${this.adminUnitId}`, { + withCredentials: true, + handleLoading: this.handleLoadingAdminUnit, + }) + .then((response) => { + this.adminUnit = response.data; + }); + }, + handleLoadingAdminUnit(isLoading) { + this.isLoadingAdminUnit = isLoading; + }, + submitForm() { + let data = { + 'email': this.form.email, + 'organization_name': this.form.organization_name, + 'relation_auto_verify_event_reference_requests': this.form.relation_auto_verify_event_reference_requests, + 'relation_verify': this.form.relation_verify, + }; + + axios + .post(`/api/v1/organizations/${this.adminUnitId}/organization-invitations`, + data, + { + withCredentials: true, + handleLoading: this.handleSubmitting, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.successMessage")) + this.goBack() + }) + }, + handleSubmitting(isLoading) { + this.isSubmitting = isLoading; + }, + goBack() { + this.$root.goBack(`/manage/admin_unit/${this.adminUnitId}/organization-invitations`) + }, + } +}; diff --git a/project/static/vue/organization-organization-invitations/list.vue.js b/project/static/vue/organization-organization-invitations/list.vue.js new file mode 100644 index 0000000..0867268 --- /dev/null +++ b/project/static/vue/organization-organization-invitations/list.vue.js @@ -0,0 +1,137 @@ +const OrganizationOrganizationInvitationList = { + template: ` +
+

{{ $t("comp.title") }}

+ +
+ {{ $t("comp.addTitle") }} +
+ + + + + + + +
+ `, + i18n: { + messages: { + en: { + comp: { + title: "Invitations", + addTitle: "Add invitation", + deletedMessage: "Invitation successfully deleted", + deleteConfirmation: "Do you really want to delete the invitation?", + }, + }, + de: { + comp: { + title: "Einladungen", + addTitle: "Einladung hinzufügen", + deletedMessage: "Einladung erfolgreich gelöscht", + deleteConfirmation: "Möchtest du die Einladung wirklich löschen?", + }, + }, + }, + }, + data: () => ({ + errorMessage: null, + fields: [ + { + key: "email", + label: i18n.t("shared.models.adminUnitInvitation.email"), + }, + { + key: "organization_name", + label: i18n.t("shared.models.adminUnitInvitation.organizationName"), + }, + ], + totalRows: 0, + currentPage: 1, + perPage: 10, + searchResult: { + items: [], + }, + }), + computed: { + adminUnitId() { + return this.$route.params.admin_unit_id; + }, + }, + methods: { + loadTableData(ctx, callback) { + const vm = this; + axios + .get(`/api/v1/organizations/${this.adminUnitId}/organization-invitations`, { + params: { + page: ctx.currentPage, + per_page: ctx.perPage, + }, + withCredentials: true, + handler: this, + }) + .then((response) => { + vm.totalRows = response.data.total; + callback(response.data.items); + }) + .catch(() => { + callback([]); + }); + return null; + }, + refreshTableData() { + this.$refs.table.refresh(); + }, + handleRequestStart() { + this.errorMessage = null; + }, + handleRequestError(error, message) { + this.errorMessage = message; + }, + editItem(id) { + this.$router.push({ + path: `/manage/admin_unit/${this.adminUnitId}/organization-invitations/${id}/update`, + }); + }, + deleteItem(id) { + if (confirm(this.$t("comp.deleteConfirmation"))) { + axios + .delete(`/api/v1/organization-invitation/${id}`, { + withCredentials: true, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.deletedMessage")); + this.refreshTableData(); + }); + } + }, + }, +}; diff --git a/project/static/vue/organization-organization-invitations/update.vue.js b/project/static/vue/organization-organization-invitations/update.vue.js new file mode 100644 index 0000000..434e996 --- /dev/null +++ b/project/static/vue/organization-organization-invitations/update.vue.js @@ -0,0 +1,142 @@ +const OrganizationOrganizationInvitationUpdate = { + template: ` +
+

{{ $t("comp.title") }}

+ +
+

{{ invitation.email }}

+ + + + + + {{ $t("shared.cancel") }} + + + {{ $t("shared.submit") }} + + + +
+
+
+ `, + i18n: { + messages: { + en: { + comp: { + title: "Update invitation", + successMessage: "Relation successfully updated", + }, + }, + de: { + comp: { + title: "Einladung aktualisieren", + successMessage: "Einladung erfolgreich aktualisiert", + }, + }, + }, + }, + data: () => ({ + isLoading: false, + isLoadingAdminUnit: false, + isSubmitting: false, + invitation: null, + adminUnit: null, + form: { + organization_name: null, + relation_auto_verify_event_reference_requests: false, + relation_verify: true, + }, + }), + computed: { + adminUnitId() { + return this.$route.params.admin_unit_id; + }, + invitationId() { + return this.$route.params.organization_invitation_id; + }, + }, + mounted() { + this.isLoading = false; + this.invitation = null; + this.adminUnit = null; + this.form = { + organization_name: null, + relation_auto_verify_event_reference_requests: false, + relation_verify: true, + }; + this.loadFormData(); + this.loadAdminUnit(); + }, + methods: { + loadFormData() { + axios + .get(`/api/v1/organization-invitation/${this.invitationId}`, { + withCredentials: true, + handleLoading: this.handleLoading, + }) + .then((response) => { + this.invitation = response.data; + this.form = { + organization_name: response.data.organization_name, + relation_auto_verify_event_reference_requests: response.data.relation_auto_verify_event_reference_requests, + relation_verify: response.data.relation_verify, + }; + }); + }, + handleLoading(isLoading) { + this.isLoading = isLoading; + }, + loadAdminUnit() { + axios + .get(`/api/v1/organizations/${this.adminUnitId}`, { + withCredentials: true, + handleLoading: this.handleLoadingAdminUnit, + }) + .then((response) => { + this.adminUnit = response.data; + }); + }, + handleLoadingAdminUnit(isLoading) { + this.isLoadingAdminUnit = isLoading; + }, + submitForm() { + let data = { + 'organization_name': this.form.organization_name, + 'relation_auto_verify_event_reference_requests': this.form.relation_auto_verify_event_reference_requests, + 'relation_verify': this.form.relation_verify, + }; + + axios + .put(`/api/v1/organization-invitation/${this.invitationId}`, data, { + withCredentials: true, + handleLoading: this.handleSubmitting, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.successMessage")); + this.goBack(); + }); + }, + handleSubmitting(isLoading) { + this.isSubmitting = isLoading; + }, + goBack() { + this.$root.goBack(`/manage/admin_unit/${this.adminUnitId}/organization-invitations`); + }, + }, +}; diff --git a/project/static/vue/organization-relations/create.vue.js b/project/static/vue/organization-relations/create.vue.js index 8b37bd6..1749e37 100644 --- a/project/static/vue/organization-relations/create.vue.js +++ b/project/static/vue/organization-relations/create.vue.js @@ -13,16 +13,18 @@ const OrganizationRelationCreate = { labelKey="shared.models.adminUnitRelation.targetOrganization" :serializer="i => i.name" /> - - - {{ $t("shared.models.adminUnitRelation.autoVerifyEventReferenceRequests") }} - - - - - {{ $t("shared.models.adminUnitRelation.verify") }} - - + + {{ $t("shared.cancel") }} diff --git a/project/static/vue/organization-relations/update.vue.js b/project/static/vue/organization-relations/update.vue.js index ca15f2f..6b489d4 100644 --- a/project/static/vue/organization-relations/update.vue.js +++ b/project/static/vue/organization-relations/update.vue.js @@ -3,20 +3,22 @@ const OrganizationRelationUpdate = {

{{ $t("comp.title") }}

-
-

{{ relation.source_organization.name }}

+
+

{{ relation.target_organization.name }}

- - - {{ $t("shared.models.adminUnitRelation.autoVerifyEventReferenceRequests") }} - - - - - {{ $t("shared.models.adminUnitRelation.verify") }} - - + + {{ $t("shared.cancel") }} diff --git a/project/static/vue/user-organization-invitations/read.vue.js b/project/static/vue/user-organization-invitations/read.vue.js new file mode 100644 index 0000000..2f50659 --- /dev/null +++ b/project/static/vue/user-organization-invitations/read.vue.js @@ -0,0 +1,84 @@ +const UserOrganizationInvitationRead = { + template: ` +
+

{{ $t("comp.title") }}

+
+ +
+

{{ $t("comp.instruction", { name: invitation.organization.name }) }}

+
+ {{ $t("comp.accept") }}… + {{ $t("shared.decline") }} +
+
+
+
+
+ `, + i18n: { + messages: { + en: { + comp: { + title: "Invitation", + instruction: "{name} invited you to create an organization.", + accept: "Create organization", + declinedMessage: "Invitation successfully declined", + declineConfirmation: "Do you really want to decline the invitation?", + }, + }, + de: { + comp: { + title: "Einladung", + instruction: "{name} hat dich eingeladen, eine Organisation zu erstellen.", + accept: "Organisation erstellen", + declinedMessage: "Einladung erfolgreich abgelehnt", + declineConfirmation: "Möchtest du die Einladung wirklich ablehnen?", + }, + }, + }, + }, + data: () => ({ + isLoading: false, + invitation: null, + }), + computed: { + invitationId() { + return this.$route.params.organization_invitation_id; + }, + }, + mounted() { + this.isLoading = false; + this.invitation = null; + this.loadData(); + }, + methods: { + loadData() { + axios + .get(`/api/v1/user/organization-invitation/${this.invitationId}`, { + withCredentials: true, + handleLoading: this.handleLoading, + }) + .then((response) => { + this.invitation = response.data; + }); + }, + handleLoading(isLoading) { + this.isLoading = isLoading; + }, + accept() { + window.location.href = `/admin_unit/create?invitation_id=${this.invitationId}`; + }, + decline() { + if (confirm(this.$t("comp.declineConfirmation"))) { + axios + .delete(`/api/v1/user/organization-invitation/${this.invitationId}`, { + withCredentials: true, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.declinedMessage")); + this.$root.goBack(`/manage/admin_units`); + }); + } + }, + }, +}; diff --git a/project/templates/_macros.html b/project/templates/_macros.html index 1879fec..aa1ea53 100644 --- a/project/templates/_macros.html +++ b/project/templates/_macros.html @@ -1570,9 +1570,8 @@ $('#allday').on('change', function() { } }); - $("#name").blur(function() { + function suggest_short_name() { if ($("#short_name").val().length == 0) { - var name = $("#name").val().toLowerCase().replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss'); var re = /\w/g; var suggestion = (name.match(re) || []).join(''); @@ -1580,8 +1579,16 @@ $('#allday').on('change', function() { $("#short_name").val(suggestion); $("#short_name").valid(); } + } + + $("#name").blur(function() { + suggest_short_name(); }); + if ($("#name").val().length > 0) { + suggest_short_name(); + } + $('#location_search').on('place_changed', function() { $('#location_search').closest('.card-body').find(':input').valid(); $('#location_search').removeClass('is-valid'); diff --git a/project/templates/admin/update_admin_unit.html b/project/templates/admin/update_admin_unit.html index 988834f..60c52fa 100644 --- a/project/templates/admin/update_admin_unit.html +++ b/project/templates/admin/update_admin_unit.html @@ -18,6 +18,7 @@ {{ render_field_with_errors(form.incoming_reference_requests_allowed, ri="switch") }} {{ render_field_with_errors(form.suggestions_enabled, ri="switch") }} {{ render_field_with_errors(form.can_create_other, ri="switch") }} + {{ render_field_with_errors(form.can_invite_other, ri="switch") }} {{ render_field_with_errors(form.can_verify_other, ri="switch") }}
diff --git a/project/templates/admin_unit/create.html b/project/templates/admin_unit/create.html index ba255f9..de2e844 100644 --- a/project/templates/admin_unit/create.html +++ b/project/templates/admin_unit/create.html @@ -20,7 +20,7 @@ {% block content %}

{{ _('Create organization') }}

-
+ {{ form.hidden_tag() }}
diff --git a/project/templates/email/organization_invitation_accepted_notice.html b/project/templates/email/organization_invitation_accepted_notice.html new file mode 100644 index 0000000..783e70c --- /dev/null +++ b/project/templates/email/organization_invitation_accepted_notice.html @@ -0,0 +1,7 @@ +{% extends "email/layout.html" %} +{% from "_macros.html" import render_email_button %} +{% block content %} +

{{ _('%(email)s accepted your invitation to create the organisation %(admin_unit_name)s.', email=invitation.email, admin_unit_name=admin_unit.name) }}

+{% set path = relation.id ~ '/update' %} +{{ render_email_button(url_for('manage_admin_unit_relations', id=invitation.admin_unit_id, path=path, _external=True), _('Click here to view the relation')) }} +{% endblock %} \ No newline at end of file diff --git a/project/templates/email/organization_invitation_accepted_notice.txt b/project/templates/email/organization_invitation_accepted_notice.txt new file mode 100644 index 0000000..340803d --- /dev/null +++ b/project/templates/email/organization_invitation_accepted_notice.txt @@ -0,0 +1,4 @@ +{{ _('%(email)s accepted your invitation to create the organisation %(admin_unit_name)s.', email=invitation.email, admin_unit_name=admin_unit.name) }} +{{ _('Click the link below to view the relation') }} +{% set path = relation.id ~ '/update' %} +{{ url_for('manage_admin_unit_relations', id=invitation.admin_unit_id, path=path, _external=True) }} diff --git a/project/templates/email/organization_invitation_notice.html b/project/templates/email/organization_invitation_notice.html new file mode 100644 index 0000000..33fa2c7 --- /dev/null +++ b/project/templates/email/organization_invitation_notice.html @@ -0,0 +1,6 @@ +{% extends "email/layout.html" %} +{% from "_macros.html" import render_email_button %} +{% block content %} +

{{ _('%(admin_unit_name)s invited you to create an organization.', admin_unit_name=invitation.adminunit.name) }}

+{{ render_email_button(url_for('user_organization_invitation', id=invitation.id, _external=True), _('Click here to view the invitation')) }} +{% endblock %} \ No newline at end of file diff --git a/project/templates/email/organization_invitation_notice.txt b/project/templates/email/organization_invitation_notice.txt new file mode 100644 index 0000000..f894c5d --- /dev/null +++ b/project/templates/email/organization_invitation_notice.txt @@ -0,0 +1,3 @@ +{{ _('%(admin_unit_name)s invited you to create an organization.', admin_unit_name=invitation.adminunit.name) }} +{{ _('Click the link below to view the invitation') }} +{{ url_for('user_organization_invitation', id=invitation.id, _external=True) }} diff --git a/project/templates/layout.html b/project/templates/layout.html index 98c49b4..6b72797 100644 --- a/project/templates/layout.html +++ b/project/templates/layout.html @@ -263,6 +263,9 @@ {{ _('Members') }} {{ _('Relations') }} + {% if current_admin_unit.can_invite_other %} + {{ _('Organization invitations') }} + {% endif %} {{ _('Settings') }} {{ _('Widgets') }}
diff --git a/project/templates/layout_vue.html b/project/templates/layout_vue.html index aa4d351..7917ab9 100644 --- a/project/templates/layout_vue.html +++ b/project/templates/layout_vue.html @@ -26,6 +26,7 @@ + {% block component_scripts %} @@ -41,6 +42,7 @@ Vue.component("custom-typeahead", CustomTypeahead); Vue.component("validated-input", ValidatedInput); + Vue.component("validated-switch", ValidatedSwitch); Vue.component("validated-textarea", ValidatedTextarea); {% block component_definitions %} @@ -53,7 +55,18 @@ adminUnitRelation: { targetOrganization: "Other organization", autoVerifyEventReferenceRequests: "Verify reference requests automatically", + autoVerifyEventReferenceRequestsDescription: "If set, all upcoming reference requests of the other organization are verified automatically.", verify: "Verify other organization", + verifyDescription: "If set, events of the other organization are publicly visible.", + }, + adminUnitInvitation: { + email: "Email", + emailDescription: "The invitation will be sent to this email address.", + organizationName: "New organization's name", + relationAutoVerifyEventReferenceRequests: "Verify reference requests automatically", + relationAutoVerifyEventReferenceRequestsDescription: "If set, all upcoming reference requests of the new organization are verified automatically.", + relationVerify: "Verify new organization", + relationVerifyDescription: "If set, events of the new organization are publicly visible.", }, eventReport: { contactName: "Name", @@ -64,6 +77,7 @@ }, }, cancel: "Cancel", + decline: "Decline", submit: "Submit", edit: "Edit", delete: "Delete", @@ -121,7 +135,18 @@ targetOrganization: "Andere Organisation", autoVerifyEventReferenceRequests: "Empfehlungsanfragen automatisch verifizieren", + autoVerifyEventReferenceRequestsDescription: "Wenn gesetzt, werden alle zukünftigen Empfehlungsanfragen der anderen Organisation automatisch verifiziert.", verify: "Andere Organisation verifizieren", + verifyDescription: "Wenn gesetzt, sind Veranstaltungen der anderen Organisation öffentlich sichtbar.", + }, + adminUnitInvitation: { + email: "Email", + emailDescription: "An diese Email-Adresse wird die Einladung gesendet.", + organizationName: "Name der neuen Organisation", + relationAutoVerifyEventReferenceRequests: "Empfehlungsanfragen automatisch verifizieren", + relationAutoVerifyEventReferenceRequestsDescription: "Wenn gesetzt, werden alle zukünftigen Empfehlungsanfragen der neuen Organisation automatisch verifiziert.", + relationVerify: "Neue Organisation verifizieren", + relationVerifyDescription: "Wenn gesetzt, sind Veranstaltungen der neuen Organisation öffentlich sichtbar.", }, eventReport: { contactName: "Name", @@ -132,6 +157,7 @@ }, }, cancel: "Abbrechen", + decline: "Ablehnen", submit: "Senden", edit: "Bearbeiten", delete: "Löschen", diff --git a/project/templates/manage/admin_units.html b/project/templates/manage/admin_units.html index 0d8d519..e087303 100644 --- a/project/templates/manage/admin_units.html +++ b/project/templates/manage/admin_units.html @@ -6,16 +6,28 @@ {% if invitations %}

{{ _('Invitations') }}

-
+
{% for invitation in invitations %} {{ invitation.adminunit.name }} {% endfor %}
{% endif %} +{% if organization_invitations %} +

{{ _('Organization invitations') }}

+
+ {% for invitation in organization_invitations %} + {{ invitation.admin_unit_name }} + {% endfor %} +
+{% endif %} +

{{ _('Organizations') }}

- {{ _('Create organization') }} + {{ _('Create organization') }} + {% if current_admin_unit and current_admin_unit.can_invite_other %} + {{ _('Invite organization') }} + {% endif %}
diff --git a/project/templates/manage/organization_invitations.html b/project/templates/manage/organization_invitations.html new file mode 100644 index 0000000..00acee0 --- /dev/null +++ b/project/templates/manage/organization_invitations.html @@ -0,0 +1,34 @@ +{% extends "layout_vue.html" %} + +{%- block title -%} +{{ _('Organization invitations') }} +{%- endblock -%} + +{% block component_scripts %} + + + +{% endblock %} + +{% block component_definitions %} +Vue.component("OrganizationOrganizationInvitationList", OrganizationOrganizationInvitationList); +Vue.component("OrganizationOrganizationInvitationCreate", OrganizationOrganizationInvitationCreate); +Vue.component("OrganizationOrganizationInvitationUpdate", OrganizationOrganizationInvitationUpdate); +{% endblock %} + +{% block vue_routes %} +const routes = [ + { + path: "/manage/admin_unit/:admin_unit_id/organization-invitations", + component: OrganizationOrganizationInvitationList, + }, + { + path: "/manage/admin_unit/:admin_unit_id/organization-invitations/create", + component: OrganizationOrganizationInvitationCreate, + }, + { + path: "/manage/admin_unit/:admin_unit_id/organization-invitations/:organization_invitation_id/update", + component: OrganizationOrganizationInvitationUpdate, + }, +]; +{% endblock %} diff --git a/project/templates/profile.html b/project/templates/profile.html index 3b62626..4472bcb 100644 --- a/project/templates/profile.html +++ b/project/templates/profile.html @@ -36,46 +36,4 @@
{% endif %} -{% if invitations %} -

{{ _('Invitations') }}

-
- - - - - - - - {% for invitation in invitations %} - - - - {% endfor %} - -
{{ _('Name') }}
{{ invitation.adminunit.name }}
-
-{% endif %} - -{% if admin_unit_members %} -

{{ _('Organizations') }}

-
- - - - - - - - - {% for member in admin_unit_members %} - - - - - {% endfor %} - -
{{ _('Name') }}{{ _('Roles') }}
{{ member.adminunit.name }}{{ render_roles(member.roles)}}
-
-{% endif %} - {% endblock %} \ No newline at end of file diff --git a/project/templates/user/organization_invitations.html b/project/templates/user/organization_invitations.html new file mode 100644 index 0000000..821ecad --- /dev/null +++ b/project/templates/user/organization_invitations.html @@ -0,0 +1,22 @@ +{% extends "layout_vue.html" %} + +{%- block title -%} +{{ _('Organization invitations') }} +{%- endblock -%} + +{% block component_scripts %} + +{% endblock %} + +{% block component_definitions %} +Vue.component("UserOrganizationInvitationRead", UserOrganizationInvitationRead); +{% endblock %} + +{% block vue_routes %} +const routes = [ + { + path: "/user/organization-invitations/:organization_invitation_id", + component: UserOrganizationInvitationRead, + }, +]; +{% endblock %} diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index dc1b3b8d08e6d53035a798e7e03c4fdf5139503f..2104f570e1036fea9962b1c29bf2e1de8fba3850 100644 GIT binary patch delta 8067 zcmZA52~?I<9>?*E2%>C>iW}&Q0s;bxOBQaZ;lAY(it8&pD%-2FC}Q@MwrHarM=I0I z#x%8#w19Rr&78ER)u!xZYRhP899n zL$>}BhEsnPn;PSq_w0ky)=#iAFI+%%4C!P{JB&cplTZ_-V>8UbmN?Qr&qp71KStv! zjKz(p1wDcMF;DZaNai;uC}^M$Q7?RkX?Pi@V;fe}4Huyz+k}c>7e-(`4#O9*1qS=P ziKFm3>YcDT_CN)2BkH{z409=rp^$`AP!rYQLVOGrS$Kjm{4w$TD-Q?b4BUic@moy9 zoGy(iH4dgyFGe5UkJ{S($gAdE+x{K8EZVf`%HY@+71?A|CgxxyI;ad)pcb?O714TZ z%m}uoz6;aw5Gpg5P!nIpwiv@IwRLHz%nV8-{~ahypg|pqQ3EVPweQ5H_#$cn$5APK z6E)#UY@7(Su&byAHzS>zpbhGIvaR< zTibN$0%Az26CXy7}Q}4peDTAx*hf5I&7c6+gNA+FWUyq zt{2*&R-B2t9iveb7Go%`#pbvHTi_Oqz{gP;sK-b=hT-@Q>iI{g=a+5$XN+Qg6GlEF zv8^=;)uA_PfE?>s)Rs&`y}!`5FSYHfFpBn#sQ%kg{hva;|2%5Juc87uiEc9rXDBGr z^QhC>g#FMd4@C_SZf%P?JYDVcR2)k^6E)xp+rA2;so#TPxXHHv1&gRZj(st@C;P82 zS#D476T01c!J3-I!Qpus_Qn@55x+qlvRDp^+6Q7ET!LEQF4Pw8MZP`eRn+aef}>EK zxDaM^Z}P85D`?Q(SEEvNANp{!^?6i=&Y}+8_V^&3;h^??Y>d*~FJ5m z>d>yT^>tRamV#2X#rhm-rSIAo&Y%W5XT69z#a~!|!9?nj*LxZ4g?fJ^>d=itUCSBP z+pOC$O85Ub1^p_WM0L1|8X&lzH&GZW6YbH5$*52B2vonh)>WwfJMmVmM`ftT4PNB^ zQP()vTG%N2znX$l`xvq!Q-{jPVbq>}fa!Pzb-3E!=-Jttgxd4&*5Ot^D)qNw8(fXC zxEYnhLIPZ+D{>_6%y_$5D~LjauMGsP{j$eqo<~ zi!rnZ_4gLi8eMhlPC+ZZ(VBzN)JLG6=h?c09jI5K2HJp1=??2l)^o_QFlKQLjp ziQ3Z7P=S1ljTso^WvKNa@~_C_Xi!SK+6VnmsT_<-c`oWujzF?Pm~Azna97*E~Jq@YO0qV{MKcEnl8c`?gTk?%t-@SycIRK%xI znY(~W?T@JOB8Ga#paN-U?TmUq*{i!I+cpeA9j>XUYqA(M@ja*%uS0d*jLJ|Q_P`_9 z1;4^fY&pz}EC+RMXIuY-W2qm&EDX6xzd0_crNAGvf`298Q6va+9(AY^bG?a%U=H;q z=)-!{-k!uH`~r1WS`POz)d3ZG8fw8=s68KwIy+OaHS?PS3W~5Abr$YIMOusMxZ6HI zVCyeqBJJ;^w&Z8jR)ml6&PrPhp`L|$uODh*Gf^3Kup8cvZc_?-C}{8N(7^+!>(^kdXxFF^%bZQEB`SC1wC zTJc&M!tnqq^~X^IeSo@VpI{hXMMd%(YQj$3HC@Li(~#pfBD5)SkYF+M6@hPf;JFE7oAXt1;9&U`xzEWo|Gwo&jt{ zeI8ziOHt$9W1nw81^fs$V}7%Tf>!nnYES=;itt0!i{GOn2)f07Sg;NCG}MA~P!kSE zosoPjz&Xedo;ilO7?j6k`dvn4c-*!n74Ux(WJTGaawp%$_YHPMr(1=ZX7A**|gf>!nhYJm4q1Dv&WMf9ufu5vT>npeAgO>X(4JZfUljjqRupLq2S-nMpw%=VB}_K&|{X+kP)aC~=^n<_vCJZ$}1S)_y)HO`N2Qd=|;>)-IgXel5vJ$*m_x}h5ZkV}_?{6{A$3gfG zW~qH1KcP4jv#|z;;C|GEKVT>P1-oHKR;j}|3^ncy?2gZ(-v0>W@H{qWe)De%K5Sm- zeFxI4qfrAFqcXA#-ak6iV0XbpZx2E z2Wbe!?WhIpvDRT%>h&0p?_&ynhe~lg8ybpzQP*k!cEQn@h-H|J8&UWEAZolnLh_>l(P(_id^sD<7NJEBhOKwG~Blc-mt2Hb{Qu?{uh=pxTNRD^}71>KG6_k?}^ zHtM(GLzjY5@+B&w2u`3tFe1uaDF^&P0g^f2nZ1E@WJ0d+>+MU8U_ zbql`6v6xxn{Y<+nD5zl%DuSyRhD}Pnj^U_*TA@~)iY+kPKF`H)>f=ybF&(vlx%PP# z>Uyq31-jn0@5Sc2|NAI(x!MJ_Cp`$S*uZzZN?Nlg%M~1 z-uEQZnuywh{-{9w7_IwXPC*f@LaqEhOv4?hpVbqn@4$Jy16wch?(>7#f%;3Rg?)t4 z_^UN?vDZHym9Z@A091e@RA+uOhk{a9jM}RuNNUV-RO(lw7Pc96NS{L8_oJABCs6|i zE%DxKkLlDiQGv|B#zSbIKY?2K5p;FfPE*hVBFgPvqpnFRj=^!L4x2Fx_hAKIL_g+J zDaOOt3%gZ%*DoItGky$?pwJ_&W|{ni?5 zEo#8Ms1!HY`VmwnU&faBmaTt+ZK!{aI*h+ok$?T@#4Ys(=xrT@TG%Mm87M)e+Qn3? z!-;qb6-1~n$b5N;$6i47Yn1k(W zysgMbeF1N?^{uFgkD?Cm8>p0?LM`|os7SxF^%g6w=e+=_j0C+aMm#vHtaI^C(ac@s>*e$?+kE$kqw|7jeK=TKkJ6kWM4Nht-T zMNXiyHaRY(cG#>Kw^K^uEsGiK#N#|~QIJxWUP7uIf? z7af$@;E#JGB)Hrutv#9$-|$<)Un84phz7SuLU7A#7h*=yTk!&stTt(n;;9#^@ zY6u=VGpI2~3;hIKRIt$J6a^MIrT#)Ekgl@h^Cq+L)KvML@`_5QwAA@S#-=#sWxRY% zwi*X)93ZzSz<5=G1wMaC4H+zQ$}7`-Rb|ZgztMw9SM7#*u~EK1%+L@RwKKFGuMKr- zikyXwV+3ldYMi14%wIpTZB!^zFnVpm><$eVW>p8b{ljv-hfcr@seO7@Y{OLl%;1a) zuknxTu_^VU{QV85VMk$c^R)3*l{HRz27xq&PFE-3D|IRu`2QZ&rk5o)L?(qksVYPH4yMs7Fi7uMZ|E|K`~JyViEGHGXW#964hl9`r#_{$7gJPErw9vh)r>; zZGXe)I)9bIIN=zBdOrg_*cqE+DYn8&)Pf$tcwCBM zxCu4Rj>hM%{SXqHa}gDgUwg-Km7;J8N=<82YLZY3$wWoc6P1ZVjKXrvz!|8FY(!1C z4O`;@)RujVjTylh>OKkP{a950)C9+Ed~hEPiewII0Si#6dki(<6?p?HV?Uw-`VT6ju3w5-Nd&f`p*?DV0@R+4L>-z1wtX!s z9l&hncdk-kF-{U;D3T%8MaWHZUO_G7 zW7I;UpiLZ%3V(y{Pw(SWoFW^E($PXn?EMJE+4EOrF#s z2K77z)t-YIupfqEiG4l}!>CV3E%;$nAdh1Ju0{pA7Ij$PLANP|Jrp#-KI;+G;W>$V z{tXsk18TrbHdgI9s0HO=AQsy8;W&kQ8Fs<_s1Mi;?29EF1o_9i$^TFa-_Xz*)3eN% zZ7k|=tw6QEfjM{%wXmp8=CH+J0`;z_Yjr;s;!NZ`I(twHJ&!sQ-=i{f3q2T+P5xC# z;V3FKg{Z?e1(lJNsEA*%^$i$KeG4k3yHQ(EkDBNxGPd&tYCK<7ugoN%-pfF}mxo$N zfooqVL`^sx75NzJfLoMKKR0j5AB%Z+x`~`J(;&Y9Ojk5n~ z6!gW)vJSRRLZxgew!|l}6~2f{=`JL@&OYpkzo53RQ#VsDM+H!WdVe$OEWC+Y*e(oa ze&+}UMScRc!tYQWE?IxJ&u?M`?f!XYA8ljvVe9uu_8q_O=GQSFmEuy==^u;A$Q*2qtMD%TGis0bpbqB|)LwsSpEscT|Axv` zNIv^NfkJk^xpwPN3wawg@E52(y^MJXNo z#wkaQJE;fx*9zy;puJj#TH&*(_D!gTY)3_S5Ec1Z)O)|82K2wjEF=n}skgCqLB3he zP*lGqs1MV69E0^Pg&`E;3mj(zPDiEYV{D6;Q4s`j!s0Lj6xaiRY28yz?^Z`*9pQ>i%D&pvV$?n|s>FItzMjknb zFct%ONfRYuFC2j$T#wqy-I#>OFp&A3YZR2KJE#boF}haV61B(4sKe3?qi`T9!V1*h zPeDbx5Y_K#`@GiHU&BP&_oC)GhuVstFq-+D-zjJdTJ|?Dwn6QAFI0*Pu>)3OQ(TSO z`{!{Y)}pRklL4k*C@PSasP9ND^6_&LQGtxYY@CWNd*IYjNW-J{!3}InJ#C;FU=WgB zXE-*&b@+@AhYU4va-qr8Xr$Shg*sewP!p~|KdeT7++gdQ3dz4xxs?V*=Cu#@SU*H9 z@Bk`h|3;yC2bpUXgUVPIYTd>PgM8fxO?q2_~=g~`-MVg@e3RCKr7 z!YNcHe2UG=JD^tD8JlAP2H?H6ejkQWpM=Up73#~k9Qi0Zb*Srh8Z+^#eeM}%z8Bq* zFRJSdqo6&Tikj#t?1&ptU$!Hty}W?hl7Cr$MSXZ~TcZeHhb|q%u>h5^VhqJesIxN< zTi_~ey#F=!g_lqf??6TR9%^CxQG0nF72y@sdjV{;eiI^43r|N)n2%cUFpR`9)Y+JZ z58!O%tM8n^{+vH2qLjZ*VL2+|Kz;}!FcCFiF6z_{!AhKr_u*fWUuq|jiX4NwrZuRu z^NlsG%sd~9Eot9?+Pa0|5DQHSd} zRKMM*JwJvXyp5VTexw;c2ep;?BiVnYwjT{TM8&8H$D&T}R9mmIbr<#ilc)jK+4_s9 zey^kY?XvZHTmKBz?*yt}11jLlBgwxy{!W8Z;(x!{`zTbzv8W%XJZl*$L-SF4{3vR| zWww2_tykN69cu44quzfFwUC{t%)jqa(272>4M(jfPz(DOHNZvG06$u1# zsfSpjQ18c~7My^ZFwH*CMjcu=-!=@!yJ#pyebHv1I?lmXxCpiKHMadvs0F@^I@Q}z z6NFco{?Vuj;!pu(qOM^!uE(CpEpVN$C_G3*)M)bsdlWg?&M};dNn^On_#AdpJLaO# zSo0^IJk)8gLQV88#^d|g0Z*e2Ws`Ac+!#!!J{y}dzf(&gmWG#5sr(Q<_$ju<%hsUr zX5ds*CjNlhf>P9V9gWdA8#T^Ts6Q{P#m2qI?$mdp7WM;%FuxNq!E}g6MUZ0anWzbJ zQK{>Nx@JQ#8>{T|ZK!^4p(fsiT3|hDi#|t<^A##H=dc~#L02z$*g)-dI%=S7Yc3{G z?}2eR4pZM!-O5IS5#JQ-`zY;yT)p{5e*(FTD@ck3!Xv+@)l}g`%wXW zfh^p0&QM6B;VNq4xM}8|cfzIAAHa0HfH4?8-7G8vHDHmo0@Z&y2ICUz)2IN~+xiYv z=H9~q-T#j$1kvy*D)q-vDZPX`l($j$yZH?BtJWShU@_{w$(VtQQGvXUjfc=azloYK zY^E7M3Uv$8aIo(Gy%ZGLM${K>7tX_rI37pMGGD@dsKc5v+gz_v7)E^wCgZcHiFRQi zet^0a2eBVswCx#l%ysU9ZZjT~P|zu#V12~87B%2@R7&5m^?KA{J%r)-H(PH&W$+T} z@ZCY3iMY9@e~z^$YGH%slK;j%qal-qC76mkuo%BaMV3lBx?nMO#+5h*_hLRqSMhbk z5x5Q4qb4YrZ~j-zIP64y6Ar}V*b8G8kbmvPs0Ah^E~;LKig-VU;89dczs5*Bk1g?< zt%oc$*Q%{GA6xOf47Ig0txsY&_06b^zw1)yO5qr`M*l_TP{pIJUuV?BC76g6s0A%W z9kORohwm-aduLD+e~&BhwryYbkO}B%)RwMCZDDC0g&Yc7P-o#Z_QK1k)13COnP3Fw zQeTEz*k05Cr*Qy&hx%fsKEmJgdJm4QJ{H?H@_||NXI7|oKFVMRz_k{p&m!9W>L$dNRv-3RJox5jcd)p4G t@Top9xP>>T=oNpjPl;bMZ~6EEzTPh@OMR\n" "Language: de\n" @@ -147,34 +147,42 @@ msgid "Scope_profile" msgstr "Profil-Informationen" #: project/i10n.py:45 +msgid "Scope_user:read" +msgstr "Nutzer-Einstellungen lesen" + +#: project/i10n.py:46 +msgid "Scope_user:write" +msgstr "Nutzer-Einstellungen anlegen, ändern und löschen" + +#: project/i10n.py:47 msgid "Scope_organizer:write" msgstr "Veranstalter anlegen, ändern und löschen" -#: project/i10n.py:46 +#: project/i10n.py:48 msgid "Scope_place:write" msgstr "Orte anlegen, ändern und löschen" -#: project/i10n.py:47 +#: project/i10n.py:49 msgid "Scope_event:write" msgstr "Veranstaltungen anlegen, ändern und löschen" -#: project/i10n.py:48 +#: project/i10n.py:50 msgid "Scope_organization:read" msgstr "Organisationen lesen" -#: project/i10n.py:49 +#: project/i10n.py:51 msgid "Scope_organization:write" msgstr "Organisationen anlegen, ändern und löschen" -#: project/i10n.py:50 +#: project/i10n.py:52 msgid "There must be no self-reference." msgstr "Es darf keine Selbstreferenz geben." -#: project/utils.py:11 +#: project/utils.py:15 msgid "Event_" msgstr "Event_" -#: project/utils.py:15 +#: project/utils.py:19 msgid "." msgstr "." @@ -182,24 +190,29 @@ msgstr "." msgid "message" msgstr "message" -#: project/forms/admin.py:10 project/templates/layout.html:305 +#: project/api/organization/resources.py:348 +#: project/views/admin_unit_member_invitation.py:92 +msgid "You have received an invitation" +msgstr "Du hast eine Einladung erhalten" + +#: project/forms/admin.py:10 project/templates/layout.html:308 #: project/views/root.py:42 msgid "Terms of service" msgstr "Nutzungsbedingungen" -#: project/forms/admin.py:11 project/templates/layout.html:309 +#: project/forms/admin.py:11 project/templates/layout.html:312 #: project/views/root.py:50 msgid "Legal notice" msgstr "Impressum" #: project/forms/admin.py:12 project/templates/_macros.html:1409 -#: project/templates/layout.html:313 +#: project/templates/layout.html:316 #: project/templates/widget/event_suggestion/create.html:204 -#: project/views/admin_unit.py:36 project/views/root.py:58 +#: project/views/admin_unit.py:50 project/views/root.py:58 msgid "Contact" msgstr "Kontakt" -#: project/forms/admin.py:13 project/templates/layout.html:317 +#: project/forms/admin.py:13 project/templates/layout.html:320 #: project/views/root.py:66 msgid "Privacy" msgstr "Datenschutz" @@ -209,7 +222,7 @@ msgid "Save" msgstr "Speichern" #: project/forms/admin.py:19 project/forms/admin_unit_member.py:12 -#: project/forms/admin_unit_member.py:32 project/templates/profile.html:66 +#: project/forms/admin_unit_member.py:32 msgid "Roles" msgstr "Rollen" @@ -249,16 +262,26 @@ msgstr "" "erstellen." #: project/forms/admin.py:44 +msgid "Invite other organizations" +msgstr "Andere Organisationen einladen" + +#: project/forms/admin.py:45 +msgid "If set, members of the organization can invite other organizations." +msgstr "" +"Wenn gesetzt, können Mitglieder der Organisation andere Organisationen " +"einladen." + +#: project/forms/admin.py:51 msgid "Verify other organizations" msgstr "Andere Organisationen verifizieren" -#: project/forms/admin.py:45 +#: project/forms/admin.py:52 msgid "If set, members of the organization can verify other organizations." msgstr "" "Wenn gesetzt, können Mitglieder der Organisation andere Organisationen " "verifizieren." -#: project/forms/admin.py:50 project/templates/admin/update_admin_unit.html:4 +#: project/forms/admin.py:57 project/templates/admin/update_admin_unit.html:4 #: project/templates/admin/update_admin_unit.html:8 msgid "Update organization" msgstr "Organisation aktualisieren" @@ -302,7 +325,6 @@ msgstr "Längengrad" #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/oauth2_client/list.html:25 -#: project/templates/profile.html:45 project/templates/profile.html:65 msgid "Name" msgstr "Name" @@ -352,7 +374,7 @@ msgstr "Logo" #: project/forms/admin_unit.py:63 project/templates/admin_unit/create.html:5 #: project/templates/admin_unit/create.html:22 -#: project/templates/manage/admin_units.html:18 +#: project/templates/manage/admin_units.html:27 msgid "Create organization" msgstr "Organisation erstellen" @@ -912,7 +934,7 @@ msgid "Distance" msgstr "Distanz" #: project/forms/event_date.py:32 project/forms/planing.py:36 -#: project/templates/widget/event_date/list.html:60 +#: project/templates/widget/event_date/list.html:61 msgid "Find" msgstr "Finden" @@ -1366,8 +1388,7 @@ msgstr "Beispiel" #: project/templates/admin/admin_units.html:11 #: project/templates/layout.html:186 #: project/templates/manage/admin_units.html:3 -#: project/templates/manage/admin_units.html:16 -#: project/templates/profile.html:60 +#: project/templates/manage/admin_units.html:25 msgid "Organizations" msgstr "Organisationen" @@ -1451,27 +1472,34 @@ msgstr "Mitglieder" msgid "Relations" msgstr "Beziehungen" +#: project/templates/layout.html:267 +#: project/templates/manage/admin_units.html:17 +#: project/templates/manage/organization_invitations.html:4 +#: project/templates/user/organization_invitations.html:4 +msgid "Organization invitations" +msgstr "Organisationseinladungen" + #: project/templates/admin/admin.html:15 #: project/templates/admin/settings.html:4 #: project/templates/admin/settings.html:8 #: project/templates/admin_unit/update.html:6 #: project/templates/admin_unit/update.html:23 -#: project/templates/layout.html:266 project/templates/manage/widgets.html:12 +#: project/templates/layout.html:269 project/templates/manage/widgets.html:12 #: project/templates/profile.html:19 msgid "Settings" msgstr "Einstellungen" -#: project/templates/layout.html:267 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:270 project/templates/manage/reviews.html:10 #: project/templates/manage/widgets.html:5 #: project/templates/manage/widgets.html:9 msgid "Widgets" msgstr "Widgets" -#: project/templates/layout.html:277 +#: project/templates/layout.html:280 msgid "Switch organization" msgstr "Organisation wechseln" -#: project/templates/developer/read.html:4 project/templates/layout.html:327 +#: project/templates/developer/read.html:4 project/templates/layout.html:330 #: project/templates/profile.html:29 msgid "Developer" msgstr "Entwickler" @@ -1487,11 +1515,6 @@ msgstr "Apps" msgid "OAuth2 clients" msgstr "OAuth2 Clients" -#: project/templates/manage/admin_units.html:8 -#: project/templates/manage/members.html:9 project/templates/profile.html:40 -msgid "Invitations" -msgstr "Einladungen" - #: project/templates/admin/admin.html:23 project/templates/admin/users.html:4 #: project/templates/admin/users.html:11 msgid "Users" @@ -1539,6 +1562,7 @@ msgid "You have been invited to join %(admin_unit_name)s." msgstr "Du wurdest eingeladen, %(admin_unit_name)s beizutreten." #: project/templates/email/invitation_notice.html:5 +#: project/templates/email/organization_invitation_notice.html:5 msgid "Click here to view the invitation" msgstr "Klicke hier, um die Einladung anzunehmen." @@ -1550,6 +1574,24 @@ msgstr "Moin" msgid "this is a message from Oveda - Die offene Veranstaltungsdatenbank." msgstr "das ist eine Nachricht von Oveda - Die offene Veranstaltungsdatenbank." +#: project/templates/email/organization_invitation_accepted_notice.html:4 +#, python-format +msgid "" +"%(email)s accepted your invitation to create the organisation " +"%(admin_unit_name)s." +msgstr "" +"%(email)s hat deine Einladung akzeptiert, um die Organisation " +"%(admin_unit_name)s zu erstellen." + +#: project/templates/email/organization_invitation_accepted_notice.html:6 +msgid "Click here to view the relation" +msgstr "Klicke hier, um die Beziehung anzuzeigen." + +#: project/templates/email/organization_invitation_notice.html:4 +#, python-format +msgid "%(admin_unit_name)s invited you to create an organization." +msgstr "%(admin_unit_name)s hat dich eingeladen, eine Organisation zu erstellen." + #: project/templates/email/reference_auto_verified_notice.html:4 msgid "There is a new referenced event that was automatically verified." msgstr "Es gibt eine neue Empfehlung, die automatisch verifiziert wurde." @@ -1719,6 +1761,15 @@ msgstr "Einladung" msgid "Would you like to accept the invitation from %(name)s?" msgstr "Möchtest du die Einladung von %(name)s akzeptieren?" +#: project/templates/manage/admin_units.html:8 +#: project/templates/manage/members.html:9 +msgid "Invitations" +msgstr "Einladungen" + +#: project/templates/manage/admin_units.html:29 +msgid "Invite organization" +msgstr "Organisation einladen" + #: project/templates/manage/delete_member.html:13 msgid "Member" msgstr "Mitglied" @@ -1881,7 +1932,7 @@ msgstr "Du hast noch keinen Account? Kein Problem!" msgid "Widget" msgstr "Widget" -#: project/templates/widget/event_date/list.html:123 +#: project/templates/widget/event_date/list.html:124 msgid "Print" msgstr "Drucken" @@ -1901,7 +1952,7 @@ msgstr "Vorschau" msgid "Organization successfully updated" msgstr "Organisation erfolgreich aktualisiert" -#: project/views/admin.py:68 project/views/manage.py:274 +#: project/views/admin.py:68 project/views/manage.py:317 msgid "Settings successfully updated" msgstr "Einstellungen erfolgreich aktualisiert" @@ -1909,7 +1960,7 @@ msgstr "Einstellungen erfolgreich aktualisiert" msgid "User successfully updated" msgstr "Nutzer erfolgreich aktualisiert" -#: project/views/admin_unit.py:32 +#: project/views/admin_unit.py:46 msgid "" "Organizations cannot currently be created. The project is in a closed " "test phase. If you are interested, you can contact us." @@ -1918,14 +1969,18 @@ msgstr "" " sich in einer geschlossenen Test-Phase. Bei Interesse kannst du uns " "kontaktieren." -#: project/views/admin_unit.py:50 +#: project/views/admin_unit.py:79 msgid "Organization successfully created" msgstr "Organisation erfolgreich erstellt" -#: project/views/admin_unit.py:76 +#: project/views/admin_unit.py:105 msgid "AdminUnit successfully updated" msgstr "Organisation erfolgreich aktualisiert" +#: project/views/admin_unit.py:127 +msgid "Organization invitation accepted" +msgstr "Organisationseinladung akzeptiert" + #: project/views/admin_unit_member.py:43 msgid "Member successfully updated" msgstr "Mitglied erfolgreich aktualisiert" @@ -1938,27 +1993,23 @@ msgstr "Die eingegebene Email passt nicht zur Email des Mitglieds" msgid "Member successfully deleted" msgstr "Mitglied erfolgreich gelöscht" -#: project/views/admin_unit_member_invitation.py:44 +#: project/views/admin_unit_member_invitation.py:45 msgid "Invitation successfully accepted" msgstr "Einladung erfolgreich akzeptiert" -#: project/views/admin_unit_member_invitation.py:51 +#: project/views/admin_unit_member_invitation.py:52 msgid "Invitation successfully declined" msgstr "Einladung erfolgreich abgelehnt" -#: project/views/admin_unit_member_invitation.py:91 -msgid "You have received an invitation" -msgstr "Du hast eine Einladung erhalten" - -#: project/views/admin_unit_member_invitation.py:96 +#: project/views/admin_unit_member_invitation.py:97 msgid "Invitation successfully sent" msgstr "Einladung erfolgreich gesendet" -#: project/views/admin_unit_member_invitation.py:119 +#: project/views/admin_unit_member_invitation.py:120 msgid "Entered email does not match invitation email" msgstr "Die eingegebene Email passt nicht zur Email der Einladung" -#: project/views/admin_unit_member_invitation.py:124 +#: project/views/admin_unit_member_invitation.py:125 msgid "Invitation successfully deleted" msgstr "Einladung erfolgreich gelöscht" diff --git a/project/translations/en/LC_MESSAGES/messages.mo b/project/translations/en/LC_MESSAGES/messages.mo index 1867ea9978332962605340953a8746d1a8cb13e1..fe321774e3348c669ead84a36c673b0473bc6e21 100644 GIT binary patch delta 24 fcmZ1~wNz?@0VlVifv%y6f}w$xiQ#5z&R5I;QQrn? delta 24 fcmZ1~wNz?@0VlVCrLK{sf|0S6fzf7b&R5I;QmzJh diff --git a/project/translations/en/LC_MESSAGES/messages.po b/project/translations/en/LC_MESSAGES/messages.po index a4cd1dd..bfeecd5 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: 2021-09-29 23:02+0200\n" +"POT-Creation-Date: 2021-10-14 10:41+0200\n" "PO-Revision-Date: 2021-04-30 15:04+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -147,34 +147,42 @@ msgid "Scope_profile" msgstr "Profile information" #: project/i10n.py:45 +msgid "Scope_user:read" +msgstr "" + +#: project/i10n.py:46 +msgid "Scope_user:write" +msgstr "" + +#: project/i10n.py:47 msgid "Scope_organizer:write" msgstr "Create, update and delete organizers" -#: project/i10n.py:46 +#: project/i10n.py:48 msgid "Scope_place:write" msgstr "Create, update and delete places" -#: project/i10n.py:47 +#: project/i10n.py:49 msgid "Scope_event:write" msgstr "Create, update and delete events" -#: project/i10n.py:48 +#: project/i10n.py:50 msgid "Scope_organization:read" msgstr "Read organizations" -#: project/i10n.py:49 +#: project/i10n.py:51 msgid "Scope_organization:write" msgstr "Create, update and delete organizations" -#: project/i10n.py:50 +#: project/i10n.py:52 msgid "There must be no self-reference." msgstr "" -#: project/utils.py:11 +#: project/utils.py:15 msgid "Event_" msgstr "" -#: project/utils.py:15 +#: project/utils.py:19 msgid "." msgstr "" @@ -182,24 +190,29 @@ msgstr "" msgid "message" msgstr "" -#: project/forms/admin.py:10 project/templates/layout.html:305 +#: project/api/organization/resources.py:348 +#: project/views/admin_unit_member_invitation.py:92 +msgid "You have received an invitation" +msgstr "" + +#: project/forms/admin.py:10 project/templates/layout.html:308 #: project/views/root.py:42 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:309 +#: project/forms/admin.py:11 project/templates/layout.html:312 #: project/views/root.py:50 msgid "Legal notice" msgstr "" #: project/forms/admin.py:12 project/templates/_macros.html:1409 -#: project/templates/layout.html:313 +#: project/templates/layout.html:316 #: project/templates/widget/event_suggestion/create.html:204 -#: project/views/admin_unit.py:36 project/views/root.py:58 +#: project/views/admin_unit.py:50 project/views/root.py:58 msgid "Contact" msgstr "" -#: project/forms/admin.py:13 project/templates/layout.html:317 +#: project/forms/admin.py:13 project/templates/layout.html:320 #: project/views/root.py:66 msgid "Privacy" msgstr "" @@ -209,7 +222,7 @@ msgid "Save" msgstr "" #: project/forms/admin.py:19 project/forms/admin_unit_member.py:12 -#: project/forms/admin_unit_member.py:32 project/templates/profile.html:66 +#: project/forms/admin_unit_member.py:32 msgid "Roles" msgstr "" @@ -245,14 +258,22 @@ msgid "If set, members of the organization can create other organizations." msgstr "" #: project/forms/admin.py:44 -msgid "Verify other organizations" +msgid "Invite other organizations" msgstr "" #: project/forms/admin.py:45 +msgid "If set, members of the organization can invite other organizations." +msgstr "" + +#: project/forms/admin.py:51 +msgid "Verify other organizations" +msgstr "" + +#: project/forms/admin.py:52 msgid "If set, members of the organization can verify other organizations." msgstr "" -#: project/forms/admin.py:50 project/templates/admin/update_admin_unit.html:4 +#: project/forms/admin.py:57 project/templates/admin/update_admin_unit.html:4 #: project/templates/admin/update_admin_unit.html:8 msgid "Update organization" msgstr "" @@ -296,7 +317,6 @@ msgstr "" #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/oauth2_client/list.html:25 -#: project/templates/profile.html:45 project/templates/profile.html:65 msgid "Name" msgstr "" @@ -343,7 +363,7 @@ msgstr "" #: project/forms/admin_unit.py:63 project/templates/admin_unit/create.html:5 #: project/templates/admin_unit/create.html:22 -#: project/templates/manage/admin_units.html:18 +#: project/templates/manage/admin_units.html:27 msgid "Create organization" msgstr "" @@ -877,7 +897,7 @@ msgid "Distance" msgstr "" #: project/forms/event_date.py:32 project/forms/planing.py:36 -#: project/templates/widget/event_date/list.html:60 +#: project/templates/widget/event_date/list.html:61 msgid "Find" msgstr "" @@ -1325,8 +1345,7 @@ msgstr "" #: project/templates/admin/admin_units.html:11 #: project/templates/layout.html:186 #: project/templates/manage/admin_units.html:3 -#: project/templates/manage/admin_units.html:16 -#: project/templates/profile.html:60 +#: project/templates/manage/admin_units.html:25 msgid "Organizations" msgstr "" @@ -1410,27 +1429,34 @@ msgstr "" msgid "Relations" msgstr "" +#: project/templates/layout.html:267 +#: project/templates/manage/admin_units.html:17 +#: project/templates/manage/organization_invitations.html:4 +#: project/templates/user/organization_invitations.html:4 +msgid "Organization invitations" +msgstr "" + #: project/templates/admin/admin.html:15 #: project/templates/admin/settings.html:4 #: project/templates/admin/settings.html:8 #: project/templates/admin_unit/update.html:6 #: project/templates/admin_unit/update.html:23 -#: project/templates/layout.html:266 project/templates/manage/widgets.html:12 +#: project/templates/layout.html:269 project/templates/manage/widgets.html:12 #: project/templates/profile.html:19 msgid "Settings" msgstr "" -#: project/templates/layout.html:267 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:270 project/templates/manage/reviews.html:10 #: project/templates/manage/widgets.html:5 #: project/templates/manage/widgets.html:9 msgid "Widgets" msgstr "" -#: project/templates/layout.html:277 +#: project/templates/layout.html:280 msgid "Switch organization" msgstr "" -#: project/templates/developer/read.html:4 project/templates/layout.html:327 +#: project/templates/developer/read.html:4 project/templates/layout.html:330 #: project/templates/profile.html:29 msgid "Developer" msgstr "" @@ -1446,11 +1472,6 @@ msgstr "" msgid "OAuth2 clients" msgstr "" -#: project/templates/manage/admin_units.html:8 -#: project/templates/manage/members.html:9 project/templates/profile.html:40 -msgid "Invitations" -msgstr "" - #: project/templates/admin/admin.html:23 project/templates/admin/users.html:4 #: project/templates/admin/users.html:11 msgid "Users" @@ -1498,6 +1519,7 @@ msgid "You have been invited to join %(admin_unit_name)s." msgstr "" #: project/templates/email/invitation_notice.html:5 +#: project/templates/email/organization_invitation_notice.html:5 msgid "Click here to view the invitation" msgstr "" @@ -1509,6 +1531,22 @@ msgstr "" msgid "this is a message from Oveda - Die offene Veranstaltungsdatenbank." msgstr "" +#: project/templates/email/organization_invitation_accepted_notice.html:4 +#, python-format +msgid "" +"%(email)s accepted your invitation to create the organisation " +"%(admin_unit_name)s." +msgstr "" + +#: project/templates/email/organization_invitation_accepted_notice.html:6 +msgid "Click here to view the relation" +msgstr "" + +#: project/templates/email/organization_invitation_notice.html:4 +#, python-format +msgid "%(admin_unit_name)s invited you to create an organization." +msgstr "" + #: project/templates/email/reference_auto_verified_notice.html:4 msgid "There is a new referenced event that was automatically verified." msgstr "" @@ -1676,6 +1714,15 @@ msgstr "" msgid "Would you like to accept the invitation from %(name)s?" msgstr "" +#: project/templates/manage/admin_units.html:8 +#: project/templates/manage/members.html:9 +msgid "Invitations" +msgstr "" + +#: project/templates/manage/admin_units.html:29 +msgid "Invite organization" +msgstr "" + #: project/templates/manage/delete_member.html:13 msgid "Member" msgstr "" @@ -1836,7 +1883,7 @@ msgstr "" msgid "Widget" msgstr "" -#: project/templates/widget/event_date/list.html:123 +#: project/templates/widget/event_date/list.html:124 msgid "Print" msgstr "" @@ -1856,7 +1903,7 @@ msgstr "" msgid "Organization successfully updated" msgstr "" -#: project/views/admin.py:68 project/views/manage.py:274 +#: project/views/admin.py:68 project/views/manage.py:317 msgid "Settings successfully updated" msgstr "" @@ -1864,20 +1911,24 @@ msgstr "" msgid "User successfully updated" msgstr "" -#: project/views/admin_unit.py:32 +#: project/views/admin_unit.py:46 msgid "" "Organizations cannot currently be created. The project is in a closed " "test phase. If you are interested, you can contact us." msgstr "" -#: project/views/admin_unit.py:50 +#: project/views/admin_unit.py:79 msgid "Organization successfully created" msgstr "" -#: project/views/admin_unit.py:76 +#: project/views/admin_unit.py:105 msgid "AdminUnit successfully updated" msgstr "" +#: project/views/admin_unit.py:127 +msgid "Organization invitation accepted" +msgstr "" + #: project/views/admin_unit_member.py:43 msgid "Member successfully updated" msgstr "" @@ -1890,27 +1941,23 @@ msgstr "" msgid "Member successfully deleted" msgstr "" -#: project/views/admin_unit_member_invitation.py:44 +#: project/views/admin_unit_member_invitation.py:45 msgid "Invitation successfully accepted" msgstr "" -#: project/views/admin_unit_member_invitation.py:51 +#: project/views/admin_unit_member_invitation.py:52 msgid "Invitation successfully declined" msgstr "" -#: project/views/admin_unit_member_invitation.py:91 -msgid "You have received an invitation" -msgstr "" - -#: project/views/admin_unit_member_invitation.py:96 +#: project/views/admin_unit_member_invitation.py:97 msgid "Invitation successfully sent" msgstr "" -#: project/views/admin_unit_member_invitation.py:119 +#: project/views/admin_unit_member_invitation.py:120 msgid "Entered email does not match invitation email" msgstr "" -#: project/views/admin_unit_member_invitation.py:124 +#: project/views/admin_unit_member_invitation.py:125 msgid "Invitation successfully deleted" msgstr "" diff --git a/project/utils.py b/project/utils.py index 868867f..9794de9 100644 --- a/project/utils.py +++ b/project/utils.py @@ -7,6 +7,10 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.base import NO_CHANGE, object_state +def strings_are_equal_ignoring_case(string1: str, string2: str): + return string1 and string2 and string1.casefold() == string2.casefold() + + def get_event_category_name(category): return lazy_gettext("Event_" + category.name) diff --git a/project/views/admin_unit.py b/project/views/admin_unit.py index 1802b62..a1a1a12 100644 --- a/project/views/admin_unit.py +++ b/project/views/admin_unit.py @@ -1,4 +1,4 @@ -from flask import flash, redirect, render_template, url_for +from flask import flash, redirect, render_template, request, url_for from flask_babelex import gettext from flask_security import auth_required, current_user from sqlalchemy.exc import SQLAlchemyError @@ -7,16 +7,19 @@ from project import app, db from project.access import ( can_create_admin_unit, get_admin_unit_for_manage_or_404, + get_admin_unit_members_with_permission, has_access, ) from project.forms.admin_unit import CreateAdminUnitForm, UpdateAdminUnitForm -from project.models import AdminUnit, Location +from project.models import AdminUnit, AdminUnitInvitation, AdminUnitRelation, Location from project.services.admin_unit import insert_admin_unit_for_user +from project.utils import strings_are_equal_ignoring_case from project.views.utils import ( flash_errors, flash_message, handleSqlError, permission_missing, + send_mails, ) @@ -27,7 +30,18 @@ def update_admin_unit_with_form(admin_unit, form): @app.route("/admin_unit/create", methods=("GET", "POST")) @auth_required() def admin_unit_create(): - if not can_create_admin_unit(): + invitation = None + + invitation_id = ( + int(request.args.get("invitation_id")) if "invitation_id" in request.args else 0 + ) + if invitation_id > 0: + invitation = AdminUnitInvitation.query.get_or_404(invitation_id) + + if not strings_are_equal_ignoring_case(invitation.email, current_user.email): + return permission_missing(url_for("manage_admin_units")) + + if not invitation and not can_create_admin_unit(): flash_message( gettext( "Organizations cannot currently be created. The project is in a closed test phase. If you are interested, you can contact us." @@ -40,13 +54,28 @@ def admin_unit_create(): form = CreateAdminUnitForm() + if invitation and not form.is_submitted(): + form.name.data = invitation.admin_unit_name + if form.validate_on_submit(): admin_unit = AdminUnit() admin_unit.location = Location() update_admin_unit_with_form(admin_unit, form) try: - insert_admin_unit_for_user(admin_unit, current_user) + _, _, relation = insert_admin_unit_for_user( + admin_unit, current_user, invitation + ) + + if relation: + send_admin_unit_invitation_accepted_mails( + invitation, relation, admin_unit + ) + + if invitation: + db.session.delete(invitation) + db.session.commit() + flash(gettext("Organization successfully created"), "success") return redirect(url_for("manage_admin_unit", id=admin_unit.id)) except SQLAlchemyError as e: @@ -82,3 +111,22 @@ def admin_unit_update(id): flash_errors(form) return render_template("admin_unit/update.html", form=form, admin_unit=admin_unit) + + +def send_admin_unit_invitation_accepted_mails( + invitation: AdminUnitInvitation, relation: AdminUnitRelation, admin_unit: AdminUnit +): + # Benachrichtige alle Mitglieder der AdminUnit, die diese Einladung erstellt hatte + members = get_admin_unit_members_with_permission( + invitation.admin_unit_id, "admin_unit:update" + ) + emails = list(map(lambda member: member.user.email, members)) + + send_mails( + emails, + gettext("Organization invitation accepted"), + "organization_invitation_accepted_notice", + invitation=invitation, + relation=relation, + admin_unit=admin_unit, + ) diff --git a/project/views/admin_unit_member_invitation.py b/project/views/admin_unit_member_invitation.py index 3399a08..357f572 100644 --- a/project/views/admin_unit_member_invitation.py +++ b/project/views/admin_unit_member_invitation.py @@ -13,6 +13,7 @@ from project.forms.admin_unit_member import ( from project.models import AdminUnitMemberInvitation, AdminUnitMemberRole from project.services.admin_unit import add_user_to_admin_unit_with_roles from project.services.user import find_user_by_email +from project.utils import strings_are_equal_ignoring_case from project.views.utils import ( flash_errors, handleSqlError, @@ -33,7 +34,7 @@ def admin_unit_member_invitation(id): if not current_user.is_authenticated: return app.login_manager.unauthorized() - if invitation.email != current_user.email: + if not strings_are_equal_ignoring_case(invitation.email, current_user.email): return permission_missing(url_for("profile")) form = NegotiateAdminUnitMemberInvitationForm() diff --git a/project/views/manage.py b/project/views/manage.py index eec5be3..45fcb9b 100644 --- a/project/views/manage.py +++ b/project/views/manage.py @@ -22,7 +22,10 @@ from project.models import ( EventSuggestion, User, ) -from project.services.admin_unit import get_admin_unit_member_invitations +from project.services.admin_unit import ( + get_admin_unit_member_invitations, + get_admin_unit_organization_invitations, +) from project.services.event import get_events_query from project.services.event_search import EventSearchParams from project.services.event_suggestion import get_event_reviews_query @@ -53,15 +56,37 @@ def manage(): if "from_login" in request.args: admin_units = get_admin_units_for_manage() invitations = get_admin_unit_member_invitations(current_user.email) + organization_invitations = get_admin_unit_organization_invitations( + current_user.email + ) - if len(admin_units) == 1 and len(invitations) == 0: + if ( + len(admin_units) == 1 + and len(invitations) == 0 + and len(organization_invitations) == 0 + ): return redirect(url_for("manage_admin_unit", id=admin_units[0].id)) - if len(admin_units) == 0 and len(invitations) == 1: + if ( + len(admin_units) == 0 + and len(invitations) == 1 + and len(organization_invitations) == 0 + ): return redirect( url_for("admin_unit_member_invitation", id=invitations[0].id) ) + if ( + len(admin_units) == 0 + and len(invitations) == 0 + and len(organization_invitations) == 1 + ): + return redirect( + url_for( + "user_organization_invitation", id=organization_invitations[0].id + ) + ) + return redirect(url_for("manage_admin_units")) @@ -70,13 +95,18 @@ def manage(): def manage_admin_units(): admin_units = get_admin_units_for_manage() invitations = get_admin_unit_member_invitations(current_user.email) + organization_invitations = get_admin_unit_organization_invitations( + current_user.email + ) admin_units.sort(key=lambda x: x.name) invitations.sort(key=lambda x: x.adminunit.name) + organization_invitations.sort(key=lambda x: x.adminunit.name) return render_template( "manage/admin_units.html", invitations=invitations, + organization_invitations=organization_invitations, admin_units=admin_units, ) @@ -234,6 +264,19 @@ def manage_admin_unit_relations(id, path=None): ) +@app.route("/manage/admin_unit//organization-invitations") +@app.route("/manage/admin_unit//organization-invitations/") +@auth_required() +def manage_admin_unit_organization_invitations(id, path=None): + admin_unit = get_admin_unit_for_manage_or_404(id) + g.manage_admin_unit_id = id + + return render_template( + "manage/organization_invitations.html", + admin_unit=admin_unit, + ) + + @app.route("/manage/admin_unit//widgets", methods=("GET", "POST")) @auth_required() def manage_admin_unit_widgets(id): diff --git a/project/views/user.py b/project/views/user.py index 2583362..06c0995 100644 --- a/project/views/user.py +++ b/project/views/user.py @@ -1,17 +1,33 @@ -from flask import render_template +from flask import redirect, render_template, url_for from flask_security import auth_required, current_user from project import app -from project.models import AdminUnitMember -from project.services.admin_unit import get_admin_unit_member_invitations +from project.models import AdminUnitInvitation +from project.services.user import find_user_by_email @app.route("/profile") @auth_required() def profile(): - admin_unit_members = AdminUnitMember.query.filter_by(user_id=current_user.id).all() - invitations = get_admin_unit_member_invitations(current_user.email) + return render_template("profile.html") - return render_template( - "profile.html", admin_unit_members=admin_unit_members, invitations=invitations - ) + +@app.route("/user/organization-invitations/") +def user_organization_invitation(id): + invitation = AdminUnitInvitation.query.get_or_404(id) + + # Wenn Email nicht als Nutzer vorhanden, dann direkt zu Registrierung + if not find_user_by_email(invitation.email): + return redirect(url_for("security.register")) + + if not current_user.is_authenticated: + return app.login_manager.unauthorized() + + return render_template("user/organization_invitations.html") + + +@app.route("/user/organization-invitations") +@app.route("/user/organization-invitations/") +@auth_required() +def user_organization_invitations(path=None): + return render_template("user/organization_invitations.html") diff --git a/tests/api/test_organization.py b/tests/api/test_organization.py index c3820d8..9a4a2da 100644 --- a/tests/api/test_organization.py +++ b/tests/api/test_organization.py @@ -372,3 +372,57 @@ def test_outgoing_relation_post_selfReference(client, app, seeder, utils): response = utils.post_json(url, data) utils.assert_response_bad_request(response) + + +def test_organization_invitation_list(client, seeder, utils): + user_id, admin_unit_id = seeder.setup_base() + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_organization_invitation_list", + id=admin_unit_id, + ) + response = utils.get_json(url) + utils.assert_response_ok(response) + assert len(response.json["items"]) == 1 + assert response.json["items"][0]["id"] == invitation_id + assert response.json["items"][0]["email"] == "invited@test.de" + assert response.json["items"][0]["organization_name"] == "Invited Organization" + + +def test_organization_invitation_list_post(client, app, seeder, utils, mocker): + mail_mock = utils.mock_send_mails(mocker) + _, admin_unit_id = seeder.setup_api_access() + + url = utils.get_url( + "api_v1_organization_organization_invitation_list", + id=admin_unit_id, + ) + data = { + "email": "invited@test.de", + "organization_name": "Invited Organization", + "relation_auto_verify_event_reference_requests": True, + "relation_verify": True, + } + + response = utils.post_json(url, data) + utils.assert_response_created(response) + assert "id" in response.json + invitation_id = int(response.json["id"]) + + with app.app_context(): + from project.models import AdminUnitInvitation + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is not None + assert invitation.admin_unit_id == admin_unit_id + assert invitation.email == "invited@test.de" + assert invitation.admin_unit_name == "Invited Organization" + assert invitation.relation_auto_verify_event_reference_requests + assert invitation.relation_verify + + invitation_url = utils.get_url( + "user_organization_invitation", + id=invitation_id, + ) + utils.assert_send_mail_called(mail_mock, "invited@test.de", invitation_url) diff --git a/tests/api/test_organization_invitation.py b/tests/api/test_organization_invitation.py new file mode 100644 index 0000000..d147908 --- /dev/null +++ b/tests/api/test_organization_invitation.py @@ -0,0 +1,87 @@ +def test_read(client, seeder, utils): + user_id, admin_unit_id = seeder.setup_api_access() + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_invitation", + id=invitation_id, + ) + response = utils.get_json(url) + utils.assert_response_ok(response) + assert response.json["id"] == invitation_id + assert response.json["organization"]["id"] == admin_unit_id + assert response.json["email"] == "invited@test.de" + assert response.json["organization_name"] == "Invited Organization" + assert response.json["relation_auto_verify_event_reference_requests"] is False + assert response.json["relation_verify"] is False + + +def test_put(client, app, seeder, utils): + _, admin_unit_id = seeder.setup_api_access() + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + data = { + "organization_name": "Invited Organization1", + "relation_auto_verify_event_reference_requests": True, + "relation_verify": True, + } + + url = utils.get_url( + "api_v1_organization_invitation", + id=invitation_id, + ) + response = utils.put_json(url, data) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import AdminUnitInvitation + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is not None + assert invitation.admin_unit_id == admin_unit_id + assert invitation.email == "invited@test.de" + assert invitation.admin_unit_name == "Invited Organization1" + assert invitation.relation_auto_verify_event_reference_requests + assert invitation.relation_verify + + +def test_patch(client, app, seeder, utils): + _, admin_unit_id = seeder.setup_api_access() + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + data = { + "relation_auto_verify_event_reference_requests": True, + } + + url = utils.get_url( + "api_v1_organization_invitation", + id=invitation_id, + ) + response = utils.patch_json(url, data) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import AdminUnitInvitation + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is not None + assert invitation.admin_unit_id == admin_unit_id + assert invitation.relation_auto_verify_event_reference_requests + + +def test_delete(client, app, seeder, utils): + _, admin_unit_id = seeder.setup_api_access() + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_invitation", + id=invitation_id, + ) + response = utils.delete(url) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import AdminUnitInvitation + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is None diff --git a/tests/api/test_user.py b/tests/api/test_user.py new file mode 100644 index 0000000..2a25f06 --- /dev/null +++ b/tests/api/test_user.py @@ -0,0 +1,65 @@ +def test_organization_invitation_list(client, seeder, utils): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("invited@test.de") + utils.login("invited@test.de") + + url = utils.get_url("api_v1_user_organization_invitation_list") + response = utils.get_json(url) + utils.assert_response_ok(response) + assert len(response.json["items"]) == 1 + assert response.json["items"][0]["id"] == invitation_id + assert response.json["items"][0]["email"] == "invited@test.de" + assert response.json["items"][0]["organization_name"] == "Invited Organization" + + +def test_organization_invitation_read(client, seeder, utils): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("invited@test.de") + utils.login("invited@test.de") + + url = utils.get_url("api_v1_user_organization_invitation", id=invitation_id) + response = utils.get_json(url) + utils.assert_response_ok(response) + assert response.json["id"] == invitation_id + assert response.json["organization"]["id"] == admin_unit_id + assert response.json["email"] == "invited@test.de" + assert response.json["organization_name"] == "Invited Organization" + assert response.json["relation_auto_verify_event_reference_requests"] is False + assert response.json["relation_verify"] is False + + +def test_organization_invitation_read_wrongEmail(client, seeder, utils): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("other@test.de") + utils.login("other@test.de") + + url = utils.get_url("api_v1_user_organization_invitation", id=invitation_id) + response = utils.get_json(url) + utils.assert_response_unauthorized(response) + + +def test_organization_invitation_delete(client, app, seeder, utils): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("invited@test.de") + utils.login("invited@test.de") + + url = utils.get_url( + "api_v1_user_organization_invitation", + id=invitation_id, + ) + response = utils.delete(url) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import AdminUnitInvitation + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is None diff --git a/tests/seeder.py b/tests/seeder.py index c82fb32..c0eef1b 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -52,6 +52,7 @@ class Seeder(object): can_create_other=False, can_verify_other=False, verified=False, + can_invite_other=True, ): from project.models import AdminUnit from project.services.admin_unit import insert_admin_unit_for_user @@ -66,6 +67,7 @@ class Seeder(object): admin_unit.suggestions_enabled = suggestions_enabled admin_unit.can_create_other = can_create_other admin_unit.can_verify_other = can_verify_other + admin_unit.can_invite_other = can_invite_other insert_admin_unit_for_user(admin_unit, user) self._db.session.commit() admin_unit_id = admin_unit.id @@ -123,6 +125,31 @@ class Seeder(object): return invitation_id + def create_admin_unit_invitation( + self, + admin_unit_id, + email="invited@test.de", + admin_unit_name="Invited Organization", + relation_auto_verify_event_reference_requests=False, + relation_verify=False, + ): + from project.models import AdminUnitInvitation + + with self._app.app_context(): + invitation = AdminUnitInvitation() + invitation.admin_unit_id = admin_unit_id + invitation.email = email + invitation.admin_unit_name = admin_unit_name + invitation.relation_auto_verify_event_reference_requests = ( + relation_auto_verify_event_reference_requests + ) + invitation.relation_verify = relation_verify + self._db.session.add(invitation) + self._db.session.commit() + invitation_id = invitation.id + + return invitation_id + def create_admin_unit_member_event_verifier(self, admin_unit_id): return self.create_admin_unit_member(admin_unit_id, ["event_verifier"]) diff --git a/tests/test_models.py b/tests/test_models.py index c6de422..359c89d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,4 @@ -from project.models import AdminUnitRelation +from project.models import AdminUnitInvitation, AdminUnitRelation def test_location_update_coordinate(client, app, db): @@ -227,3 +227,21 @@ def test_admin_unit_verification(client, app, db, seeder): all_verified = AdminUnit.query.filter(AdminUnit.is_verified).all() assert len(all_verified) == 0 + + +def test_admin_unit_invitations(client, app, db, seeder): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + with app.app_context(): + from project.services.admin_unit import get_admin_unit_by_id + + admin_unit = get_admin_unit_by_id(admin_unit_id) + assert len(admin_unit.admin_unit_invitations) == 1 + + admin_unit.can_invite_other = False + db.session.commit() + + assert len(admin_unit.admin_unit_invitations) == 0 + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is None diff --git a/tests/views/test_admin.py b/tests/views/test_admin.py index 55eac83..cbcdf4e 100644 --- a/tests/views/test_admin.py +++ b/tests/views/test_admin.py @@ -120,6 +120,7 @@ def test_admin_admin_unit_update(client, seeder, utils, app, mocker, db, db_erro admin_unit.incoming_reference_requests_allowed = False admin_unit.suggestions_enabled = False admin_unit.can_create_other = False + admin_unit.can_invite_other = False admin_unit.can_verify_other = False db.session.commit() @@ -136,6 +137,7 @@ def test_admin_admin_unit_update(client, seeder, utils, app, mocker, db, db_erro "incoming_reference_requests_allowed": "y", "suggestions_enabled": "y", "can_create_other": "y", + "can_invite_other": "y", "can_verify_other": "y", }, ) @@ -153,4 +155,5 @@ def test_admin_admin_unit_update(client, seeder, utils, app, mocker, db, db_erro assert admin_unit.incoming_reference_requests_allowed assert admin_unit.suggestions_enabled assert admin_unit.can_create_other + assert admin_unit.can_invite_other assert admin_unit.can_verify_other diff --git a/tests/views/test_admin_unit.py b/tests/views/test_admin_unit.py index 1ca3285..8e58107 100644 --- a/tests/views/test_admin_unit.py +++ b/tests/views/test_admin_unit.py @@ -134,6 +134,77 @@ def test_create_requiresAdmin_memberOfOrgWithFlag(client, app, utils, seeder): assert response.status_code == 302 +def test_create_from_invitation(client, app, utils, seeder, mocker): + mail_mock = utils.mock_send_mails(mocker) + user_id = seeder.create_user() + admin_unit_id = seeder.create_admin_unit( + user_id, can_invite_other=True, can_verify_other=True + ) + invitation_id = seeder.create_admin_unit_invitation( + admin_unit_id, + relation_auto_verify_event_reference_requests=True, + relation_verify=True, + ) + + seeder.create_user("invited@test.de") + utils.login("invited@test.de") + url = utils.get_url("admin_unit_create", invitation_id=invitation_id) + response = utils.get_ok(url) + + response = utils.post_form( + url, + response, + { + "short_name": "invitedorganization", + "location-postalCode": "38640", + "location-city": "Goslar", + }, + ) + assert response.status_code == 302 + + with app.app_context(): + from project.models import AdminUnitInvitation + from project.services.admin_unit import get_admin_unit_by_name + + admin_unit = get_admin_unit_by_name("Invited Organization") + assert admin_unit is not None + + relation = admin_unit.incoming_relations[0] + assert relation.source_admin_unit_id == admin_unit_id + assert relation.auto_verify_event_reference_requests + assert relation.verify + assert relation.invited + relation_id = relation.id + + invitation = AdminUnitInvitation.query.get(invitation_id) + assert invitation is None + + invitation_url = utils.get_url( + "manage_admin_unit_relations", id=admin_unit_id, path=f"{relation_id}/update" + ) + utils.assert_send_mail_called(mail_mock, "test@test.de", invitation_url) + + +def test_create_from_invitation_currentUserDoesNotMatchInvitationEmail( + client, app, utils, seeder +): + user_id = seeder.create_user() + admin_unit_id = seeder.create_admin_unit( + user_id, can_invite_other=True, can_verify_other=True + ) + invitation_id = seeder.create_admin_unit_invitation( + admin_unit_id, + relation_auto_verify_event_reference_requests=True, + relation_verify=True, + ) + + seeder.create_user("other@test.de") + utils.login("other@test.de") + url = utils.get_url("admin_unit_create", invitation_id=invitation_id) + response = utils.get(url) + utils.assert_response_redirect(response, "manage_admin_units") + + def test_update(client, app, utils, seeder): seeder.create_user() user_id = utils.login() diff --git a/tests/views/test_manage.py b/tests/views/test_manage.py index 9593991..5221143 100644 --- a/tests/views/test_manage.py +++ b/tests/views/test_manage.py @@ -48,6 +48,25 @@ def test_index_after_login(client, app, db, utils, seeder): ) +def test_index_after_login_organization_invitation(client, app, db, utils, seeder): + user_id = seeder.create_user() + admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") + + email = "invited@test.de" + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id, email) + + seeder.create_user(email) + utils.login(email) + + response = utils.get_endpoint("manage_after_login") + utils.assert_response_redirect(response, "manage", from_login=1) + + response = utils.get_endpoint("manage", from_login=1) + utils.assert_response_redirect( + response, "user_organization_invitation", id=invitation_id + ) + + def test_admin_unit(client, seeder, utils): user_id, admin_unit_id = seeder.setup_base() @@ -169,3 +188,10 @@ def test_admin_unit_relations(client, seeder, utils): url = utils.get_url("manage_admin_unit_relations", id=admin_unit_id) utils.get_ok(url) + + +def test_admin_unit_organization_invitations(client, seeder, utils): + user_id, admin_unit_id = seeder.setup_base() + + url = utils.get_url("manage_admin_unit_organization_invitations", id=admin_unit_id) + utils.get_ok(url) diff --git a/tests/views/test_user.py b/tests/views/test_user.py index 38ef52e..fa67f9b 100644 --- a/tests/views/test_user.py +++ b/tests/views/test_user.py @@ -4,3 +4,34 @@ def test_profile(client, seeder, utils): url = utils.get_url("profile", id=1) utils.get_ok(url) + + +def test_organization_invitation_not_registered(client, app, utils, seeder): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + url = utils.get_url("user_organization_invitation", id=invitation_id) + response = client.get(url) + utils.assert_response_redirect(response, "security.register") + + +def test_organization_invitation_not_authenticated(client, app, utils, seeder): + _, admin_unit_id = seeder.setup_base(log_in=False) + invitation_id = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("invited@test.de") + url = utils.get_url("user_organization_invitation", id=invitation_id) + response = client.get(url) + assert response.status_code == 302 + assert response.headers["Location"].startswith("http://localhost/login") + + +def test_organization_invitation_list(client, seeder, utils): + _, admin_unit_id = seeder.setup_base(log_in=False) + _ = seeder.create_admin_unit_invitation(admin_unit_id) + + seeder.create_user("invited@test.de") + utils.login("invited@test.de") + + url = utils.get_url("user_organization_invitations") + utils.get_ok(url)