Skip to content

Commit

Permalink
Refacoted duplicate invitation mails. Part of @1260
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Apr 3, 2024
1 parent 58cb79e commit ea93fe4
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 124 deletions.
15 changes: 9 additions & 6 deletions client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
}


Expand Down Expand Up @@ -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
Expand Down
106 changes: 45 additions & 61 deletions client/src/pages/NewInvitation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()] : [];
Expand All @@ -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",
Expand Down Expand Up @@ -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")}];
});
}

Expand All @@ -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});
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -214,12 +207,10 @@ class NewInvitation extends React.Component {
}
};

preview = disabledSubmit => (
<div>
preview = disabledSubmit => (<div>
<div className={"preview-mail"} dangerouslySetInnerHTML={{__html: this.state.htmlPreview}}/>
{this.renderActions(disabledSubmit)}
</div>
);
</div>);

selectedGroupsChanged = selectedOptions => {
if (selectedOptions === null) {
Expand All @@ -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) =>
<div className={"invitation-form"}>

<EmailField addEmails={this.addEmails}
Expand Down Expand Up @@ -286,19 +276,16 @@ class NewInvitation extends React.Component {
maxDate={moment().add(31, "day").toDate()}
name={I18n.t("invitation.expiryDate")}
toolTip={I18n.t("invitation.expiryDateTooltip")}/>
{(!initial && isEmpty(expiry_date)) &&
<ErrorIndicator msg={I18n.t("invitation.requiredExpiryDate")}/>}
{(!initial && isEmpty(expiry_date)) && <ErrorIndicator msg={I18n.t("invitation.requiredExpiryDate")}/>}

{this.renderActions(disabledSubmit)}
</div>;

renderActions = disabledSubmit => (
<section className="actions">
renderActions = disabledSubmit => (<section className="actions">
<Button cancelButton={true} txt={I18n.t("forms.cancel")} onClick={this.cancel}/>
<Button disabled={disabledSubmit} txt={I18n.t("invitation.invite")}
onClick={this.submit}/>
</section>
);
</section>);

render() {
const {
Expand Down Expand Up @@ -326,23 +313,20 @@ class NewInvitation extends React.Component {
return <SpinnerField/>
}
const disabledSubmit = (!initial && !this.isValid());
return (
<>
<UnitHeader obj={collaboration}
name={collaboration.name}/>
<ConfirmationDialog isOpen={confirmationDialogOpen}
cancel={cancelDialogAction}
confirm={confirmationDialogAction}
leavePage={leavePage}/>
<div className="mod-new-collaboration-invitation">
<h2>{I18n.t("tabs.invitation_form")}</h2>
<div className="new-collaboration-invitation">
{this.invitationForm(fileInputKey, fileName, fileTypeError, fileEmails, initial,
administrators, intended_role, message, expiry_date, disabledSubmit, groups,
selectedGroup, membership_expiry_date, existingInvitations)}
</div>
return (<>
<UnitHeader obj={collaboration}
name={collaboration.name}/>
<ConfirmationDialog isOpen={confirmationDialogOpen}
cancel={cancelDialogAction}
confirm={confirmationDialogAction}
leavePage={leavePage}/>
<div className="mod-new-collaboration-invitation">
<h2>{I18n.t("tabs.invitation_form")}</h2>
<div className="new-collaboration-invitation">
{this.invitationForm(fileInputKey, fileName, fileTypeError, fileEmails, initial, administrators, intended_role, message, expiry_date, disabledSubmit, groups, selectedGroup, membership_expiry_date, existingInvitations)}
</div>
</>);
</div>
</>);
}

}
Expand Down
17 changes: 9 additions & 8 deletions client/src/pages/NewOrganisationInvitation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
24 changes: 20 additions & 4 deletions client/src/pages/NewServiceInvitation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -50,7 +50,8 @@ class NewServiceInvitation extends React.Component {
leavePage: true,
activeTab: "invitation_form",
htmlPreview: "",
loading: true
loading: true,
existingInvitations: []
};
}

Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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});
};

Expand Down
16 changes: 8 additions & 8 deletions server/api/invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 10 additions & 12 deletions server/api/organisation_invitation.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Loading

0 comments on commit ea93fe4

Please sign in to comment.