Skip to content

Commit

Permalink
Merge pull request #36 from ScilifelabDataCentre/templates
Browse files Browse the repository at this point in the history
Add support for email templates
  • Loading branch information
talavis authored Nov 24, 2022
2 parents 17add94 + 2e528f5 commit c93d25f
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 27 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/python-black.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
# Perform Python code linting using black
# Remember to change "./code-folder" to the folder with the code
name: Black formatting

on: [push, pull_request]

jobs:
BlackFormatting:
concurrency:
group: ${{ github.ref }}-black
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v3

- name: Check code formatting with Black
uses: psf/black@stable
with:
options: "-l 100 --check"
src: "./form_manager"
2 changes: 1 addition & 1 deletion form_manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ class Config(object):
MAIL_USE_SSL = False
MAIL_DEFAULT_SENDER = ("Form Manager", "[email protected]")

USER_FILTER = {} # Limits to users that may use the system, e.g. {"email": ["scilifelab.se"]}
USER_FILTER = {} # Limits to users that may use the system, e.g. {"email": ["scilifelab.se"]}

REVERSE_PROXY = False # Behind a reverse proxy, use X_Forwarded-For to get the ip
17 changes: 6 additions & 11 deletions form_manager/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Endpoints related to forms."""
import json
import pprint
from bson import ObjectId

Expand All @@ -18,6 +17,10 @@ def form():
"title": "",
"recaptcha_secret": "",
"email_recipients": [],
"email_custom": False,
"email_title": "",
"email_html_template": "",
"email_text_template": "",
"owners": [],
"redirect": "",
}
Expand Down Expand Up @@ -154,7 +157,7 @@ def delete_form(identifier: str):
flask.abort(code=403)
flask.g.db["forms"].delete_one(entry)
flask.g.db["submissions"].delete_many({"identifier": entry["identifier"]})
return flask.Submission(code=200)
return ""


@csrf.exempt
Expand Down Expand Up @@ -184,15 +187,7 @@ def receive_submission(identifier: str):
del form_submission["g-recaptcha-response"]

if form_info.get("email_recipients"):
text_body = json.dumps(form_submission, indent=2, sort_keys=True, ensure_ascii=False)
text_body += f"\n\nSubmission received: {utils.make_timestamp()}"
mail.send(
flask_mail.Message(
f"Form from {form_info.get('title')}",
body=text_body,
recipients=form_info["email_recipients"],
)
)
utils.send_email(form_info, form_submission, mail)

to_add = {
"submission": form_submission,
Expand Down
4 changes: 2 additions & 2 deletions form_manager/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def user_info():
@blueprint.route("/login")
def oidc_login():
"""Perform a login using OpenID Connect."""
redirect_uri = flask.url_for("user.oidc_authorize", _external=True)
redirect_uri = flask.url_for("user.oidc_authorize", _external=True)
return oauth.oidc_entry.authorize_redirect(redirect_uri)


Expand All @@ -31,7 +31,7 @@ def oidc_authorize():
if domain_limit:
user_email = token.get("userinfo", {}).get("email")
try:
domain = user_email[user_email.index('@')+1:]
domain = user_email[user_email.index("@") + 1 :]
except ValueError:
flask.abort(400)
if domain not in domain_limit:
Expand Down
63 changes: 63 additions & 0 deletions form_manager/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""General helper functions."""
from datetime import datetime
import functools
import json
import os
import re
import secrets

import flask
import flask_mail
import pymongo
import pytz
import requests
Expand Down Expand Up @@ -111,3 +114,63 @@ def has_form_access(username, entry):
bool: Whether the user has access.
"""
return username in entry["owners"]


def apply_template(template: str, data: dict) -> str:
"""
Fill a template with the values of the defined variables.
Variables are entered as ``{{ variable }}``.
Currently using simple text replacement, but may use Jinja in the future.
Args:
template (str): The template.
data (dict): The variables to use.
Returns:
str: The resulting text.
"""
possible_inserts = re.findall(r"{{ (.+?) }}", template)
for ins in possible_inserts:
if data.get(ins):
template = template.replace("{{ " + ins + " }}", data[ins])
return template


def gen_json_body(data: dict) -> str:
"""
Generate a email body with formatted JSON.
Args:
data (dict): The data to include.
Returns:
str: The generated body text.
"""
body = json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False)
body += f"\n\nSubmission received: {make_timestamp()}"
return body


def send_email(form_info: dict, data: dict, mail_client):
"""
Send an email with the submitted form content.
Args:
form_info (dict): Information about the form.
data (dict): The submitted form.
"""
if form_info.get("email_custom"):
body_text = apply_template(form_info.get("email_text_template", ""), data)
body_html = apply_template(form_info.get("email_html_template", ""), data)
else:
body_text = gen_json_body(data)
body_html = body_text.replace("\n", "<br/>")
mail_client.send(
flask_mail.Message(
form_info.get("email_title"),
body=body_text,
html=body_html,
recipients=form_info["email_recipients"],
)
)
3 changes: 2 additions & 1 deletion frontend/src/components/StringListEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
hide-bottom-space
outlined
:label="fieldTitle"
:rules="[ function (val) { return (evaluateValue(val) || val.length === 0) || 'No whitespace at beginning nor end and must not already exist.' }]">
:rules="[ function (val) { return (evaluateValue(val) || val.length === 0) || 'No whitespace at beginning nor end and must not already exist.' }]"
@keyup.enter="addValue"
>
<template #after>
<q-btn
icon="add"
Expand Down
136 changes: 124 additions & 12 deletions frontend/src/pages/FormBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,39 @@
/>
</q-item-section>
</q-item>
<div v-show="editData[props.key].email_recipients.length > 0">
<q-item>
<q-item-section>
<q-input
v-model="editData[props.key].email_title"
dense
outlined
label="Email title"
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-toggle
v-model="editData[props.key].email_custom"
label="Custom email template"
/>
<q-space />
<div v-show="editData[props.key].email_custom">
<q-btn
flat
label="Edit text email"
@click="openTemplateDialog(editData[props.key], 'text')"
/>
<q-btn
flat
label="Edit html email"
@click="openTemplateDialog(editData[props.key], 'html')"
/>
</div>
</q-item-section>
</q-item>
</div>
<q-item>
<q-item-section>
<str-list-editor
Expand Down Expand Up @@ -169,7 +202,7 @@
size="md"
icon="delete"
color="negative"
@click="deleteForm(props)" />
@click="confirmDelete(props)" />
<span v-show="editData[props.key].saveError" class="text-negative">Save failed</span>
</div>
</q-item-section>
Expand All @@ -179,6 +212,56 @@
</q-tr>
</template>
</q-table>
<q-dialog v-model="showEditTemplateDialog">
<q-card style="min-width: 600px">
<q-card-section class="column">
<div class="text-h5 q-mb-sm">Edit Email Template</div>
<q-input
v-model="currentEditTemplateText"
autogrow
outlined
type="textarea"
/>
</q-card-section>

<q-card-actions align="right">
<q-btn
flat
label="Keep"
color="positive"
@click="saveTemplate" />
<q-btn
v-close-popup
flat
label="Cancel"
color="grey-7" />
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showDeleteWarning">
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="fas fa-trash" color="alert" text-color="primary" />
<span class="q-ml-sm">Are you sure you want to delete this form?</span>
</q-card-section>

<q-card-actions align="right">
<q-btn
flat
:loading="isDeleting"
label="Delete"
color="negative"
class="user-edit-confirm-delete"
@click="deleteForm" />
<q-btn
v-close-popup
flat
label="Cancel"
color="grey-7" />
</q-card-actions>
</q-card>
</q-dialog>

</q-page>
</template>

Expand All @@ -197,10 +280,16 @@ export default defineComponent({
return {
entries: [],
editData: {},
isDeleting: false,
toDelete: {},
filter: '',
loading: false,
loadError: false,
saveError: false,
showEditTemplateDialog: false,
showDeleteWarning: false,
currrentEditTemplate: {},
currrentEditTemplateText: "",
currrentEditTemplateType: "",
pagination: {
rowsPerPage: 20
},
Expand Down Expand Up @@ -265,39 +354,48 @@ export default defineComponent({
gotoEntry(identifier) {
this.$router.push({name: 'FormResponses', params: {identifier: identifier}});
},
deleteForm(entry) {
console.log(this.$q.cookies)
confirmDelete(entry) {
this.toDelete = entry;
this.showDeleteWarning = true;
},
deleteForm() {
this.isDeleting = true;
this.$axios
.delete('/api/v1/form/' + entry.row.identifier,
.delete('/api/v1/form/' + this.toDelete.row.identifier,
{headers: {'X-CSRFToken': this.$q.cookies.get('_csrf_token')}})
.then(() => {
entry.expand = false;
delete this.editData[entry.row.identifier];
this.toDelete.expand = false;
this.showDeleteWarning = false;
delete this.editData[this.toDelete.row.identifier];
this.getEntries();
})
.finally(() => this.isDeleting = false);
},
addForm() {
this.$axios
.post('/api/v1/form', {}, {headers: {'X-CSRFToken': this.$q.cookies.get('_csrf_token')}})
.then(() => this.getEntries())
},
expandItem(entry) {
console.log(entry)
entry.expand = !entry.expand;
if (!(entry.key in this.editData)) {
this.editData[entry.key] = {
title: entry.row.title,
recaptcha_secret: entry.row.recaptcha_secret,
email_recipients: JSON.parse(JSON.stringify(entry.row.email_recipients)),
email_custom: entry.row.email_custom,
email_text_template: entry.row.email_text_template,
email_html_template: entry.row.email_html_template,
email_title: entry.row.email_title,
owners: JSON.parse(JSON.stringify(entry.row.owners)),
redirect: entry.row.redirect,
saving: false,
saveError: false,
}
}
},
saveEdit(entry) {
this.saveError = false;
this.editData[entry.key].saving = true;
this.editData[entry.key].saveError = false;
let outgoing = JSON.parse(JSON.stringify(this.editData[entry.key]));
Expand All @@ -310,13 +408,27 @@ export default defineComponent({
delete this.editData[entry.key];
this.getEntries();
})
.catch((err) => this.editData[entry.key].saveError = true)
.finally(() => this.editData[entry.key].saving = false);
.catch((err) => {
this.editData[entry.key].saveError = true
this.editData[entry.key].saving = false
})
},
cancelEdit(entry) {
entry.expand = false;
delete this.editData[entry.key];
},
openTemplateDialog(entry, type) {
this.showEditTemplateDialog = true;
let prop = "email_" + type + "_template";
this.currentEditTemplate = entry;
console.log(this.currentEditTemplate)
this.currentEditTemplateType = prop;
this.currentEditTemplateText = entry[prop];
},
saveTemplate() {
this.currentEditTemplate[this.currentEditTemplateType] = this.currentEditTemplateText;
this.showEditTemplateDialog = false;
},
},
})
</script>
Empty file added test/__init__.py
Empty file.
Loading

0 comments on commit c93d25f

Please sign in to comment.