diff --git a/client/src/api/index.js b/client/src/api/index.js
index 9774f10dd..7f0a8aaf6 100644
--- a/client/src/api/index.js
+++ b/client/src/api/index.js
@@ -593,8 +593,9 @@ export function organisationInvitationDelete(organisationInvitationId, showError
return fetchDelete(`/api/organisation_invitations/${organisationInvitationId}`, showErrorDialog);
}
-export function organisationInvitationExists(email, organisationId) {
- return fetchJson(`/api/organisation_invitations/exists_email?email=${email}&organisation_id=${organisationId}`);
+export function organisationInvitationExists(emails, organisationId) {
+ const body = {emails: emails, organisation_id: organisationId}
+ return postPutJson("/api/organisation_invitations/exists_email", body, "POST");
}
//Invitations
@@ -623,8 +624,9 @@ export function invitationDelete(invitationId, showErrorDialog = true) {
return fetchDelete(`/api/invitations/${invitationId}`, showErrorDialog);
}
-export function invitationExists(email, collaborationId) {
- return fetchJson(`/api/invitations/exists_email?email=${email}&collaboration_id=${collaborationId}`);
+export function invitationExists(emails, collaborationId) {
+ const body = {emails: emails, collaboration_id: collaborationId}
+ return postPutJson("/api/organisation_invitations/exists_email", body, "POST");
}
@@ -988,8 +990,9 @@ export function serviceInvitationDelete(serviceInvitationId, showErrorDialog = t
return fetchDelete(`/api/service_invitations/${serviceInvitationId}`, showErrorDialog);
}
-export function serviceInvitationExists(email, serviceId) {
- return fetchJson(`/api/service_invitations/exists_email?email=${email}&service_id=${serviceId}`);
+export function serviceInvitationExists(emails, serviceId) {
+ const body = {emails: emails, service_id: serviceId}
+ return postPutJson("/api/service_invitations/exists_email", body, "POST");
}
//ServiceAups
diff --git a/client/src/pages/NewInvitation.jsx b/client/src/pages/NewInvitation.jsx
index 7a8098348..84b576caf 100644
--- a/client/src/pages/NewInvitation.jsx
+++ b/client/src/pages/NewInvitation.jsx
@@ -29,8 +29,7 @@ class NewInvitation extends React.Component {
constructor(props, context) {
super(props, context);
this.intendedRolesOptions = collaborationRoles.map(role => ({
- value: role,
- label: I18n.t(`collaboration.${role}`)
+ value: role, label: I18n.t(`collaboration.${role}`)
}));
const email = getParameterByName("email", window.location.search);
const administrators = !isEmpty(email) && validEmailRegExp.test(email.trim()) ? [email.trim()] : [];
@@ -51,8 +50,7 @@ class NewInvitation extends React.Component {
initial: true,
confirmationDialogOpen: false,
confirmationDialogAction: () => this.setState({confirmationDialogOpen: false}),
- cancelDialogAction: () => this.setState({confirmationDialogOpen: false},
- () => this.props.history.push(`/collaborations/${this.props.match.params.collaboration_id}`)),
+ cancelDialogAction: () => this.setState({confirmationDialogOpen: false}, () => this.props.history.push(`/collaborations/${this.props.match.params.collaboration_id}`)),
leavePage: true,
htmlPreview: "",
activeTab: "invitation_form",
@@ -86,25 +84,16 @@ class NewInvitation extends React.Component {
updateAppStore = (collaboration, user) => {
const orgManager = isUserAllowed(ROLES.ORG_MANAGER, user, collaboration.organisation_id, null);
AppStore.update(s => {
- s.breadcrumb.paths = orgManager ? [
- {path: "/", value: I18n.t("breadcrumb.home")},
- {
- path: `/organisations/${collaboration.organisation_id}`,
- value: I18n.t("breadcrumb.organisation", {name: collaboration.organisation.name})
- },
- {
- path: `/collaborations/${collaboration.id}`,
- value: I18n.t("breadcrumb.collaboration", {name: collaboration.name})
- },
- {value: I18n.t("breadcrumb.invite")}
- ] : [
- {path: "/", value: I18n.t("breadcrumb.home")},
- {
- path: `/collaborations/${collaboration.id}`,
- value: I18n.t("breadcrumb.collaboration", {name: collaboration.name})
- },
- {value: I18n.t("breadcrumb.invite")}
- ];
+ s.breadcrumb.paths = orgManager ? [{path: "/", value: I18n.t("breadcrumb.home")}, {
+ path: `/organisations/${collaboration.organisation_id}`,
+ value: I18n.t("breadcrumb.organisation", {name: collaboration.organisation.name})
+ }, {
+ path: `/collaborations/${collaboration.id}`,
+ value: I18n.t("breadcrumb.collaboration", {name: collaboration.name})
+ }, {value: I18n.t("breadcrumb.invite")}] : [{path: "/", value: I18n.t("breadcrumb.home")}, {
+ path: `/collaborations/${collaboration.id}`,
+ value: I18n.t("breadcrumb.collaboration", {name: collaboration.name})
+ }, {value: I18n.t("breadcrumb.invite")}];
});
}
@@ -119,8 +108,15 @@ class NewInvitation extends React.Component {
doSubmit = () => {
const {
- administrators, message, collaboration, expiry_date, fileEmails, intended_role,
- selectedGroup, membership_expiry_date, isAdminView
+ administrators,
+ message,
+ collaboration,
+ expiry_date,
+ fileEmails,
+ intended_role,
+ selectedGroup,
+ membership_expiry_date,
+ isAdminView
} = this.state;
if (this.isValid()) {
this.setState({loading: true});
@@ -160,14 +156,11 @@ class NewInvitation extends React.Component {
validateDuplicates(newAdministrators) {
const collaborationId = this.props.match.params.collaboration_id;
- const promises = newAdministrators.map(email => invitationExists(email, collaborationId));
- Promise.all(promises)
- .then(res => {
- const existingInvitations = res
- .filter(email => email.exists)
- .map(email => email.email);
- this.setState({existingInvitations: existingInvitations, initial: isEmpty(existingInvitations)})
- })
+ this.setState({loading: true});
+ invitationExists(newAdministrators, collaborationId)
+ .then(existingInvitations => this.setState({
+ existingInvitations: existingInvitations, initial: isEmpty(existingInvitations), loading: false
+ }))
}
addEmails = emails => {
@@ -214,12 +207,10 @@ class NewInvitation extends React.Component {
}
};
- preview = disabledSubmit => (
-
+ preview = disabledSubmit => (
{this.renderActions(disabledSubmit)}
-
- );
+
);
selectedGroupsChanged = selectedOptions => {
if (selectedOptions === null) {
@@ -230,8 +221,7 @@ class NewInvitation extends React.Component {
}
}
- invitationForm = (fileInputKey, fileName, fileTypeError, fileEmails, initial, administrators,
- intended_role, message, expiry_date, disabledSubmit, groups, selectedGroup, membership_expiry_date, existingInvitations) =>
+ invitationForm = (fileInputKey, fileName, fileTypeError, fileEmails, initial, administrators, intended_role, message, expiry_date, disabledSubmit, groups, selectedGroup, membership_expiry_date, existingInvitations) =>
- {(!initial && isEmpty(expiry_date)) &&
- }
+ {(!initial && isEmpty(expiry_date)) && }
{this.renderActions(disabledSubmit)}
;
- renderActions = disabledSubmit => (
-
+ renderActions = disabledSubmit => (
- );
+ );
render() {
const {
@@ -326,23 +313,20 @@ class NewInvitation extends React.Component {
return
}
const disabledSubmit = (!initial && !this.isValid());
- return (
- <>
-
-
-
-
{I18n.t("tabs.invitation_form")}
-
- {this.invitationForm(fileInputKey, fileName, fileTypeError, fileEmails, initial,
- administrators, intended_role, message, expiry_date, disabledSubmit, groups,
- selectedGroup, membership_expiry_date, existingInvitations)}
-
+ return (<>
+
+
+
+
{I18n.t("tabs.invitation_form")}
+
+ {this.invitationForm(fileInputKey, fileName, fileTypeError, fileEmails, initial, administrators, intended_role, message, expiry_date, disabledSubmit, groups, selectedGroup, membership_expiry_date, existingInvitations)}
- >);
+
+ >);
}
}
diff --git a/client/src/pages/NewOrganisationInvitation.jsx b/client/src/pages/NewOrganisationInvitation.jsx
index 2ae5ad415..dd82c5b84 100644
--- a/client/src/pages/NewOrganisationInvitation.jsx
+++ b/client/src/pages/NewOrganisationInvitation.jsx
@@ -139,14 +139,15 @@ class NewOrganisationInvitation extends React.Component {
validateDuplicates(newAdministrators) {
const organisationId = this.props.match.params.organisation_id;
- const promises = newAdministrators.map(email => organisationInvitationExists(email, organisationId));
- Promise.all(promises)
- .then(res => {
- const existingInvitations = res
- .filter(email => email.exists)
- .map(email => email.email);
- this.setState({existingInvitations: existingInvitations, initial: isEmpty(existingInvitations)})
- })
+ this.setState({loading: true});
+ organisationInvitationExists(newAdministrators, organisationId)
+ .then(existingInvitations =>
+ this.setState({
+ existingInvitations: existingInvitations,
+ initial: isEmpty(existingInvitations),
+ loading: false
+ })
+ );
}
tabChanged = activeTab => {
diff --git a/client/src/pages/NewServiceInvitation.jsx b/client/src/pages/NewServiceInvitation.jsx
index 2b34d338e..30bc1f372 100644
--- a/client/src/pages/NewServiceInvitation.jsx
+++ b/client/src/pages/NewServiceInvitation.jsx
@@ -3,7 +3,7 @@ import moment from "moment";
import "react-datepicker/dist/react-datepicker.css";
-import {serviceById, serviceInvitations} from "../api";
+import {serviceById, serviceInvitationExists, serviceInvitations} from "../api";
import I18n from "../locale/I18n";
import InputField from "../components/InputField";
import Button from "../components/Button";
@@ -50,7 +50,8 @@ class NewServiceInvitation extends React.Component {
leavePage: true,
activeTab: "invitation_form",
htmlPreview: "",
- loading: true
+ loading: true,
+ existingInvitations: []
};
}
@@ -81,8 +82,8 @@ class NewServiceInvitation extends React.Component {
};
isValid = () => {
- const {administrators, fileEmails} = this.state;
- return !isEmpty(administrators) || !isEmpty(fileEmails);
+ const {administrators, fileEmails, existingInvitations} = this.state;
+ return (!isEmpty(administrators) || !isEmpty(fileEmails)) && isEmpty(existingInvitations);
};
doSubmit = () => {
@@ -113,16 +114,31 @@ class NewServiceInvitation extends React.Component {
}
};
+ validateDuplicates(newAdministrators) {
+ const collaborationId = this.props.match.params.collaboration_id;
+ this.setState({loading: true});
+ serviceInvitationExists(newAdministrators, collaborationId)
+ .then(existingInvitations =>
+ this.setState({
+ existingInvitations: existingInvitations,
+ initial: isEmpty(existingInvitations),
+ loading: false
+ })
+ )
+ }
+
removeMail = email => e => {
stopEvent(e);
const {administrators} = this.state;
const newAdministrators = administrators.filter(currentMail => currentMail !== email);
+ this.validateDuplicates(newAdministrators);
this.setState({administrators: newAdministrators});
};
addEmails = emails => {
const {administrators} = this.state;
const uniqueEmails = [...new Set(administrators.concat(emails))];
+ this.validateDuplicates(uniqueEmails);
this.setState({administrators: uniqueEmails});
};
diff --git a/server/api/invitation.py b/server/api/invitation.py
index 45eeee9a7..41d677162 100644
--- a/server/api/invitation.py
+++ b/server/api/invitation.py
@@ -6,7 +6,7 @@
from flasgger import swag_from
from flask import Blueprint, request as current_request, current_app, g as request_context, jsonify
from sqlalchemy import or_, func
-from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import joinedload, load_only
from werkzeug.exceptions import Conflict, Forbidden, BadRequest
from server.api.base import json_endpoint, query_param, emit_socket
@@ -136,17 +136,17 @@ def invitations_by_hash():
return {"invitation": invitation_json, "service_emails": service_emails, "admin_emails": admin_emails}, 200
-@invitations_api.route("/exists_email", methods=["GET"], strict_slashes=False)
+@invitations_api.route("/exists_email", methods=["POST"], strict_slashes=False)
@json_endpoint
def invitation_exists_by_email():
- email = query_param("email")
- collaboration_id = int(query_param("collaboration_id"))
- invitation_first = Invitation.query \
- .filter(func.lower(Invitation.invitee_email) == email.lower()) \
+ data = current_request.get_json()
+ collaboration_id = int(data["collaboration_id"])
+ invitations = Invitation.query.options(load_only(Invitation.invitee_email)) \
+ .filter(func.lower(Invitation.invitee_email).in_([e.lower() for e in data["emails"]])) \
.filter(Invitation.collaboration_id == collaboration_id) \
.filter(Invitation.status == STATUS_OPEN) \
- .count()
- return {"exists": invitation_first > 0, "email": email}, 200
+ .all()
+ return [i.invitee_email for i in invitations], 200
@invitations_api.route("/v1/collaboration_invites", methods=["PUT"], strict_slashes=False)
diff --git a/server/api/organisation_invitation.py b/server/api/organisation_invitation.py
index 625bde0cd..0f24a35db 100644
--- a/server/api/organisation_invitation.py
+++ b/server/api/organisation_invitation.py
@@ -1,9 +1,6 @@
-from sqlalchemy import func
-
-from server.tools import dt_now
-
from flask import Blueprint, request as current_request, current_app
-from sqlalchemy.orm import joinedload
+from sqlalchemy import func
+from sqlalchemy.orm import joinedload, load_only
from werkzeug.exceptions import Conflict
from server.api.base import json_endpoint, query_param, emit_socket
@@ -12,6 +9,7 @@
from server.db.domain import OrganisationInvitation, Organisation, OrganisationMembership, db
from server.db.models import delete
from server.mail import mail_organisation_invitation
+from server.tools import dt_now
organisation_invitations_api = Blueprint("organisation_invitations_api", __name__,
url_prefix="/api/organisation_invitations")
@@ -132,13 +130,13 @@ def delete_organisation_invitation(id):
return delete(OrganisationInvitation, id)
-@organisation_invitations_api.route("/exists_email", methods=["GET"], strict_slashes=False)
+@organisation_invitations_api.route("/exists_email", methods=["POST"], strict_slashes=False)
@json_endpoint
def invitation_exists_by_email():
- email = query_param("email")
- organisation_id = int(query_param("organisation_id"))
- invitation_first = OrganisationInvitation.query \
- .filter(func.lower(OrganisationInvitation.invitee_email) == email.lower()) \
+ data = current_request.get_json()
+ organisation_id = int(data["organisation_id"])
+ invitations = OrganisationInvitation.query.options(load_only(OrganisationInvitation.invitee_email)) \
+ .filter(func.lower(OrganisationInvitation.invitee_email).in_([e.lower() for e in data["emails"]])) \
.filter(OrganisationInvitation.organisation_id == organisation_id) \
- .count()
- return {"exists": invitation_first > 0, "email": email}, 200
+ .all()
+ return [i.invitee_email for i in invitations], 200
diff --git a/server/api/service_invitation.py b/server/api/service_invitation.py
index 85b637eea..c5f2b4274 100644
--- a/server/api/service_invitation.py
+++ b/server/api/service_invitation.py
@@ -1,6 +1,6 @@
from flask import Blueprint, request as current_request, current_app
from sqlalchemy import func
-from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import joinedload, load_only
from werkzeug.exceptions import Conflict
from server.api.base import json_endpoint, query_param, emit_socket
@@ -129,11 +129,13 @@ def delete_service_invitation(id):
return delete(ServiceInvitation, id)
-@service_invitations_api.route("/exists_email", methods=["GET"], strict_slashes=False)
+@service_invitations_api.route("/exists_email", methods=["POST"], strict_slashes=False)
@json_endpoint
def invitation_exists_by_email():
- email = query_param("email")
- invitation_first = ServiceInvitation.query \
- .filter(func.lower(ServiceInvitation.invitee_email) == email.lower()) \
- .count()
- return {"exists": invitation_first > 0, "email": email}, 200
+ data = current_request.get_json()
+ service_id = int(data["service_id"])
+ invitations = ServiceInvitation.query.options(load_only(ServiceInvitation.invitee_email)) \
+ .filter(func.lower(ServiceInvitation.invitee_email).in_([e.lower() for e in data["emails"]])) \
+ .filter(ServiceInvitation.service_id == service_id) \
+ .all()
+ return [i.invitee_email for i in invitations], 200
diff --git a/server/test/api/test_invitation.py b/server/test/api/test_invitation.py
index 9c1333f37..e6c4768b8 100644
--- a/server/test/api/test_invitation.py
+++ b/server/test/api/test_invitation.py
@@ -318,9 +318,11 @@ def test_delete_external_invitation(self):
def test_invitation_exists_by_email(self):
invitation = Invitation.query.filter(Invitation.invitee_email == "curious@ex.org").one()
collaboration_id = invitation.collaboration_id
- res = self.get("/api/invitations/exists_email",
- query_data={"email": "CURIOUS@ex.org", "collaboration_id": collaboration_id})
- self.assertEqual(True, res["exists"])
- res = self.get("/api/invitations/exists_email",
- query_data={"email": "nope@ex.org", "collaboration_id": collaboration_id})
- self.assertEqual(False, res["exists"])
+ res = self.post("/api/invitations/exists_email",
+ body={"emails": ["CURIOUS@ex.org"], "collaboration_id": collaboration_id},
+ response_status_code=200)
+ self.assertEqual(["curious@ex.org"], res)
+ res = self.post("/api/invitations/exists_email",
+ body={"emails": ["nope@ex.org"], "collaboration_id": collaboration_id},
+ response_status_code=200)
+ self.assertEqual(0, len(res))
diff --git a/server/test/api/test_organisation_invitation.py b/server/test/api/test_organisation_invitation.py
index 6ee52ccc6..231b4688b 100644
--- a/server/test/api/test_organisation_invitation.py
+++ b/server/test/api/test_organisation_invitation.py
@@ -94,9 +94,15 @@ def test_delete_not_found(self):
def test_invitation_exists_by_email(self):
inv = OrganisationInvitation.query.filter(OrganisationInvitation.invitee_email == "roger@example.org").one()
organisation_id = inv.organisation_id
- res = self.get("/api/organisation_invitations/exists_email",
- query_data={"email": "roger@EXAMPLE.ORG", "organisation_id": organisation_id})
- self.assertEqual(True, res["exists"])
- res = self.get("/api/organisation_invitations/exists_email",
- query_data={"email": "nope@ex.org", "organisation_id": organisation_id})
- self.assertEqual(False, res["exists"])
+ res = self.post("/api/organisation_invitations/exists_email",
+ body={"emails": ["roger@EXAMPLE.ORG"], "organisation_id": organisation_id},
+ response_status_code=200)
+ self.assertEqual(["roger@example.org"], res)
+ res = self.post("/api/organisation_invitations/exists_email",
+ body={"emails": ["nope@ex.org"], "organisation_id": organisation_id},
+ response_status_code=200)
+ self.assertEqual(0, len(res))
+ res = self.post("/api/organisation_invitations/exists_email",
+ body={"emails": ["roger@example.org"], "organisation_id": "9999"},
+ response_status_code=200)
+ self.assertEqual(0, len(res))
diff --git a/server/test/api/test_service_invitation.py b/server/test/api/test_service_invitation.py
index c4fcb7884..90911b579 100644
--- a/server/test/api/test_service_invitation.py
+++ b/server/test/api/test_service_invitation.py
@@ -83,9 +83,12 @@ def test_delete_not_found(self):
self.delete("/api/service_invitations", primary_key="nope", response_status_code=404)
def test_invitation_exists_by_email(self):
- res = self.get("/api/service_invitations/exists_email",
- query_data={"email": "ADMIN@CLOUD.ORG"})
- self.assertEqual(True, res["exists"])
- res = self.get("/api/service_invitations/exists_email",
- query_data={"email": "nope@ex.org"})
- self.assertEqual(False, res["exists"])
+ inv = ServiceInvitation.query.filter(ServiceInvitation.invitee_email == "admin@cloud.org").one()
+ service_id = inv.service_id
+ res = self.post("/api/service_invitations/exists_email",
+ body={"emails": ["ADMIN@CLOUD.ORG", "nice@nope.com"], "service_id": service_id},
+ response_status_code=200)
+ self.assertEqual(["admin@cloud.org"], res)
+ res = self.post("/api/service_invitations/exists_email",
+ body={"emails": ["nope@ex.org"], "service_id": service_id}, response_status_code=200)
+ self.assertEqual(0, len(res))