Organisationen einladen #307

This commit is contained in:
Daniel Grams 2021-10-14 13:09:32 +02:00
parent a23ffe4ffe
commit 71028f57d1
58 changed files with 2181 additions and 229 deletions

View File

@ -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");

View File

@ -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');
});
});
});
});

View File

@ -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");

View File

@ -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');
});
});
});
});
});

View File

@ -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");
}
);

View File

@ -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();
});

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

@ -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")

View File

@ -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

View File

@ -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/<int:id>", "api_v1_organization")
add_api_resource(
OrganizationEventDateSearchResource,
@ -340,3 +395,8 @@ add_api_resource(
"/organizations/<int:id>/relations/outgoing",
"api_v1_organization_outgoing_relation_list",
)
add_api_resource(
OrganizationOrganizationInvitationListResource,
"/organizations/<int:id>/organization-invitations",
"api_v1_organization_organization_invitation_list",
)

View File

@ -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

View File

@ -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/<int:id>",
"api_v1_organization_invitation",
)

View File

@ -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()

View File

@ -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/<int:id>",
"api_v1_user_organization_invitation",
)

View File

@ -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

View File

@ -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(

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,55 @@
const ValidatedSwitch = {
template: `
<div>
<ValidationProvider :vid="vid" :name="$attrs.label" :rules="rules" v-slot="validationContext">
<b-form-group v-bind="$attrs" label="">
<b-form-checkbox switch
v-model="innerValue"
v-bind="$attrs"
:state="getValidationState(validationContext)"
>
{{ $attrs.label }}
</b-form-checkbox>
<b-form-invalid-feedback :state="getValidationState(validationContext)">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</ValidationProvider>
</div>
`,
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;
},
},
};

View File

@ -0,0 +1,127 @@
const OrganizationOrganizationInvitationCreate = {
template: `
<div>
<h1>{{ $t("comp.title") }}</h1>
<b-overlay :show="isLoadingAdminUnit">
<div v-if="adminUnit">
<ValidationObserver v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(submitForm)">
<validated-input
:label="$t('shared.models.adminUnitInvitation.email')"
:description="$t('shared.models.adminUnitInvitation.emailDescription')"
name="email"
v-model="form.email"
rules="required|email" />
<validated-input
:label="$t('shared.models.adminUnitInvitation.organizationName')"
name="organizationName"
v-model="form.organization_name"
rules="required" />
<validated-switch
v-if="adminUnit.can_verify_other"
:label="$t('shared.models.adminUnitInvitation.relationVerify')"
:description="$t('shared.models.adminUnitInvitation.relationVerifyDescription')"
name="relationVerify"
v-model="form.relation_verify" />
<validated-switch
v-if="adminUnit.incoming_reference_requests_allowed"
:label="$t('shared.models.adminUnitInvitation.relationAutoVerifyEventReferenceRequests')"
:description="$t('shared.models.adminUnitInvitation.relationAutoVerifyEventReferenceRequestsDescription')"
name="relationAutoVerifyEventReferenceRequests"
v-model="form.relation_auto_verify_event_reference_requests" />
<b-button variant="secondary" @click="goBack" v-bind:disabled="isSubmitting">{{ $t("shared.cancel") }}</b-button>
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
<b-spinner small v-if="isSubmitting"></b-spinner>
{{ $t("shared.submit") }}
</b-button>
</b-form>
</ValidationObserver>
</div>
</b-overlay>
</div>
`,
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`)
},
}
};

View File

@ -0,0 +1,137 @@
const OrganizationOrganizationInvitationList = {
template: `
<div>
<h1>{{ $t("comp.title") }}</h1>
<div class="my-4">
<b-button variant="outline-secondary" :to="{ path: 'create'}" append><i class="fa fa-plus"></i> {{ $t("comp.addTitle") }}</b-button>
</div>
<div class="alert alert-danger" role="alert" v-if="errorMessage">
{{ errorMessage }}
</div>
<b-table
ref="table"
id="main-table"
:fields="fields"
:items="loadTableData"
:current-page="currentPage"
:per-page="perPage"
primary-key="id"
thead-class="d-none"
outlined
hover
responsive
show-empty
:empty-text="$t('shared.emptyData')"
style="min-height:100px"
>
<template #cell(email)="data">
<b-dropdown :id="'item-dropdown-' + data.item.id" :text="data.value" variant="link" toggle-class="m-0 p-0">
<b-dropdown-item @click.prevent="editItem(data.item.id)">{{ $t("shared.edit") }}&hellip;</b-dropdown-item>
<b-dropdown-item @click.prevent="deleteItem(data.item.id)">{{ $t("shared.delete") }}&hellip;</b-dropdown-item>
</b-dropdown>
</template>
</b-table>
<b-pagination v-if="totalRows > 0"
v-model="currentPage"
:total-rows="totalRows"
:per-page="perPage"
aria-controls="main-table"
></b-pagination>
</div>
`,
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();
});
}
},
},
};

View File

@ -0,0 +1,142 @@
const OrganizationOrganizationInvitationUpdate = {
template: `
<div>
<h1>{{ $t("comp.title") }}</h1>
<b-overlay :show="isLoading || isLoadingAdminUnit">
<div v-if="adminUnit && invitation">
<h2 v-if="invitation">{{ invitation.email }}</h2>
<ValidationObserver v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(submitForm)">
<validated-input
:label="$t('shared.models.adminUnitInvitation.organizationName')"
name="organizationName"
v-model="form.organization_name"
rules="required" />
<validated-switch
v-if="adminUnit.can_verify_other"
:label="$t('shared.models.adminUnitInvitation.relationVerify')"
:description="$t('shared.models.adminUnitInvitation.relationVerifyDescription')"
name="relationVerify"
v-model="form.relation_verify" />
<validated-switch
v-if="adminUnit.incoming_reference_requests_allowed"
:label="$t('shared.models.adminUnitInvitation.relationAutoVerifyEventReferenceRequests')"
:description="$t('shared.models.adminUnitInvitation.relationAutoVerifyEventReferenceRequestsDescription')"
name="relationAutoVerifyEventReferenceRequests"
v-model="form.relation_auto_verify_event_reference_requests" />
<b-button variant="secondary" @click="goBack" v-bind:disabled="isSubmitting">{{ $t("shared.cancel") }}</b-button>
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
<b-spinner small v-if="isSubmitting"></b-spinner>
{{ $t("shared.submit") }}
</b-button>
</b-form>
</ValidationObserver>
</div>
</b-overlay>
</div>
`,
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`);
},
},
};

View File

@ -13,16 +13,18 @@ const OrganizationRelationCreate = {
labelKey="shared.models.adminUnitRelation.targetOrganization"
:serializer="i => i.name"
/>
<b-form-group>
<b-form-checkbox switch id="auto_verify_event_reference_requests" v-model="form.auto_verify_event_reference_requests">
{{ $t("shared.models.adminUnitRelation.autoVerifyEventReferenceRequests") }}
</b-form-checkbox>
</b-form-group>
<b-form-group v-if="adminUnit.can_verify_other">
<b-form-checkbox switch id="verify" v-model="form.verify">
{{ $t("shared.models.adminUnitRelation.verify") }}
</b-form-checkbox>
</b-form-group>
<validated-switch
v-if="adminUnit.can_verify_other"
:label="$t('shared.models.adminUnitRelation.verify')"
:description="$t('shared.models.adminUnitRelation.verifyDescription')"
name="verify"
v-model="form.verify" />
<validated-switch
v-if="adminUnit.incoming_reference_requests_allowed"
:label="$t('shared.models.adminUnitRelation.autoVerifyEventReferenceRequests')"
:description="$t('shared.models.adminUnitRelation.autoVerifyEventReferenceRequestsDescription')"
name="auto_verify_event_reference_requests"
v-model="form.auto_verify_event_reference_requests" />
<b-button variant="secondary" @click="goBack" v-bind:disabled="isSubmitting">{{ $t("shared.cancel") }}</b-button>
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
<b-spinner small v-if="isSubmitting"></b-spinner>

View File

@ -3,20 +3,22 @@ const OrganizationRelationUpdate = {
<div>
<h1>{{ $t("comp.title") }}</h1>
<b-overlay :show="isLoading || isLoadingAdminUnit">
<div v-if="adminUnit">
<h2 v-if="relation">{{ relation.source_organization.name }}</h2>
<div v-if="adminUnit && relation">
<h2 v-if="relation">{{ relation.target_organization.name }}</h2>
<ValidationObserver v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(submitForm)">
<b-form-group>
<b-form-checkbox switch id="auto_verify_event_reference_requests" v-model="form.auto_verify_event_reference_requests">
{{ $t("shared.models.adminUnitRelation.autoVerifyEventReferenceRequests") }}
</b-form-checkbox>
</b-form-group>
<b-form-group v-if="adminUnit.can_verify_other">
<b-form-checkbox switch id="verify" v-model="form.verify">
{{ $t("shared.models.adminUnitRelation.verify") }}
</b-form-checkbox>
</b-form-group>
<validated-switch
v-if="adminUnit.can_verify_other"
:label="$t('shared.models.adminUnitRelation.verify')"
:description="$t('shared.models.adminUnitRelation.verifyDescription')"
name="verify"
v-model="form.verify" />
<validated-switch
v-if="adminUnit.incoming_reference_requests_allowed"
:label="$t('shared.models.adminUnitRelation.autoVerifyEventReferenceRequests')"
:description="$t('shared.models.adminUnitRelation.autoVerifyEventReferenceRequestsDescription')"
name="auto_verify_event_reference_requests"
v-model="form.auto_verify_event_reference_requests" />
<b-button variant="secondary" @click="goBack" v-bind:disabled="isSubmitting">{{ $t("shared.cancel") }}</b-button>
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
<b-spinner small v-if="isSubmitting"></b-spinner>

View File

@ -0,0 +1,84 @@
const UserOrganizationInvitationRead = {
template: `
<div>
<h1>{{ $t("comp.title") }}</h1>
<div class="mt-3 w-normal">
<b-overlay :show="isLoading">
<div v-if="invitation">
<p>{{ $t("comp.instruction", { name: invitation.organization.name }) }}</p>
<div class="d-flex justify-content-between my-4 decision-container">
<b-button variant="success" class="m-1" @click="accept()"><i class="fa fa-check"></i> {{ $t("comp.accept") }}&hellip;</b-button>
<b-button variant="danger" class="m-1" @click="decline()"><i class="fa fa-ban"></i> {{ $t("shared.decline") }}</b-button>
</div>
</div>
</b-overlay>
</div>
</div>
`,
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`);
});
}
},
},
};

View File

@ -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');

View File

@ -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") }}
</div>
</div>

View File

@ -20,7 +20,7 @@
{% block content %}
<h1>{{ _('Create organization') }}</h1>
<form id="main-form" action="{{ url_for('admin_unit_create') }}" method="POST" enctype="multipart/form-data">
<form id="main-form" action="" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="card mb-4">

View File

@ -0,0 +1,7 @@
{% extends "email/layout.html" %}
{% from "_macros.html" import render_email_button %}
{% block content %}
<p>{{ _('%(email)s accepted your invitation to create the organisation %(admin_unit_name)s.', email=invitation.email, admin_unit_name=admin_unit.name) }}</p>
{% 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 %}

View File

@ -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) }}

View File

@ -0,0 +1,6 @@
{% extends "email/layout.html" %}
{% from "_macros.html" import render_email_button %}
{% block content %}
<p>{{ _('%(admin_unit_name)s invited you to create an organization.', admin_unit_name=invitation.adminunit.name) }}</p>
{{ render_email_button(url_for('user_organization_invitation', id=invitation.id, _external=True), _('Click here to view the invitation')) }}
{% endblock %}

View File

@ -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) }}

View File

@ -263,6 +263,9 @@
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_members', id=current_admin_unit.id) }}">{{ _('Members') }}</a>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_relations', id=current_admin_unit.id) }}">{{ _('Relations') }}</a>
{% if current_admin_unit.can_invite_other %}
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_organization_invitations', id=current_admin_unit.id) }}">{{ _('Organization invitations') }}</a>
{% endif %}
<a class="dropdown-item" href="{{ url_for('admin_unit_update', id=current_admin_unit.id) }}">{{ _('Settings') }}</a>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_widgets', id=current_admin_unit.id) }}">{{ _('Widgets') }}</a>
</div>

View File

@ -26,6 +26,7 @@
<script src="https://unpkg.com/vee-validate@3.4.11/dist/vee-validate.full.min.js"></script>
<script src="{{ url_for('static', filename='vue/common/typeahead.vue.js')}}"></script>
<script src="{{ url_for('static', filename='vue/common/validated-input.vue.js')}}"></script>
<script src="{{ url_for('static', filename='vue/common/validated-switch.vue.js')}}"></script>
<script src="{{ url_for('static', filename='vue/common/validated-textarea.vue.js')}}"></script>
{% 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",

View File

@ -6,16 +6,28 @@
{% if invitations %}
<h1>{{ _('Invitations') }}</h1>
<div class="list-group">
<div class="list-group mb-4">
{% for invitation in invitations %}
<a href="{{ url_for('admin_unit_member_invitation', id=invitation.id) }}" class="list-group-item">{{ invitation.adminunit.name }}</a>
{% endfor %}
</div>
{% endif %}
{% if organization_invitations %}
<h1>{{ _('Organization invitations') }}</h1>
<div class="list-group mb-4">
{% for invitation in organization_invitations %}
<a href="{{ url_for('user_organization_invitation', id=invitation.id) }}" class="list-group-item">{{ invitation.admin_unit_name }}</a>
{% endfor %}
</div>
{% endif %}
<h1>{{ _('Organizations') }}</h1>
<div class="my-4">
<a class="btn btn-outline-secondary my-1" href="{{ url_for('admin_unit_create') }}" role="button"><i class="fa fa-plus"></i> {{ _('Create organization') }}</a>
<a class="btn btn-outline-secondary m-1" href="{{ url_for('admin_unit_create') }}" role="button"><i class="fa fa-plus"></i> {{ _('Create organization') }}</a>
{% if current_admin_unit and current_admin_unit.can_invite_other %}
<a class="btn btn-outline-secondary m-1" href="{{ url_for('manage_admin_unit_organization_invitations', id=current_admin_unit.id, path='create') }}" role="button"><i class="fa fa-plus"></i> {{ _('Invite organization') }}</a>
{% endif %}
</div>
<div class="list-group">

View File

@ -0,0 +1,34 @@
{% extends "layout_vue.html" %}
{%- block title -%}
{{ _('Organization invitations') }}
{%- endblock -%}
{% block component_scripts %}
<script src="{{ url_for('static', filename='vue/organization-organization-invitations/list.vue.js')}}"></script>
<script src="{{ url_for('static', filename='vue/organization-organization-invitations/create.vue.js')}}"></script>
<script src="{{ url_for('static', filename='vue/organization-organization-invitations/update.vue.js')}}"></script>
{% 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 %}

View File

@ -36,46 +36,4 @@
</div>
{% endif %}
{% if invitations %}
<h2>{{ _('Invitations') }}</h2>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover table-striped">
<thead>
<tr>
<th>{{ _('Name') }}</th>
</tr>
</thead>
<tbody>
{% for invitation in invitations %}
<tr>
<td><a href="{{ url_for('admin_unit_member_invitation', id=invitation.id) }}">{{ invitation.adminunit.name }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if admin_unit_members %}
<h2>{{ _('Organizations') }}</h2>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover table-striped">
<thead>
<tr>
<th>{{ _('Name') }}</th>
<th>{{ _('Roles') }}</th>
</tr>
</thead>
<tbody>
{% for member in admin_unit_members %}
<tr>
<td><a href="{{ url_for('manage_admin_unit', id=member.adminunit.id) }}">{{ member.adminunit.name }}</a></td>
<td>{{ render_roles(member.roles)}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "layout_vue.html" %}
{%- block title -%}
{{ _('Organization invitations') }}
{%- endblock -%}
{% block component_scripts %}
<script src="{{ url_for('static', filename='vue/user-organization-invitations/read.vue.js')}}"></script>
{% endblock %}
{% block component_definitions %}
Vue.component("UserOrganizationInvitationRead", UserOrganizationInvitationRead);
{% endblock %}
{% block vue_routes %}
const routes = [
{
path: "/user/organization-invitations/:organization_invitation_id",
component: UserOrganizationInvitationRead,
},
];
{% endblock %}

View File

@ -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: 2020-06-07 18:51+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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"

View File

@ -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 <EMAIL@ADDRESS>\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 ""

View File

@ -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)

View File

@ -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,
)

View File

@ -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()

View File

@ -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/<int:id>/organization-invitations")
@app.route("/manage/admin_unit/<int:id>/organization-invitations/<path:path>")
@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/<int:id>/widgets", methods=("GET", "POST"))
@auth_required()
def manage_admin_unit_widgets(id):

View File

@ -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/<int:id>")
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/<path:path>")
@auth_required()
def user_organization_invitations(path=None):
return render_template("user/organization_invitations.html")

View File

@ -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)

View File

@ -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

65
tests/api/test_user.py Normal file
View File

@ -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

View File

@ -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"])

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)