diff --git a/Pipfile.lock b/Pipfile.lock index defcfec2..fa5c7657 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -769,7 +769,7 @@ "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.0.0" }, "geojson": { @@ -983,7 +983,7 @@ "sha256:afd7bd0d2a97bf3e2d415ad70031292d99a36e124e5a57e42cdbe803a0d77ef6", "sha256:c7b07de416fe6fe6a789c7f52a2532560e0f80fc27e9fe90e5d4386c3b6ff63b" ], - "markers": "python_version >= '3.7'", + "index": "pypi", "version": "==12.0.0b3.dev3" }, "invenio-assets": { @@ -1113,7 +1113,7 @@ "sha256:2b3d849f32f9bfd1aa636d951d952b6df2fa2b85a350080dd5a90e3ff7e56592", "sha256:7537b9902171517208552893eea3efb76fb3b55558671ea4ea6fd218d4fbc89b" ], - "markers": "python_version >= '3.8'", + "index": "pypi", "version": "==2.1.0" }, "invenio-mail": { @@ -1334,7 +1334,6 @@ "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397" ], "index": "pypi", - "markers": "python_version >= '3.9'", "version": "==8.18.1" }, "isbnlib": { @@ -1412,7 +1411,6 @@ "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==4.17.3" }, "jupyter-client": { @@ -2333,7 +2331,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-geoip": { @@ -2771,7 +2769,7 @@ "sha256:febffa5b1eda6622d44b245b0685aff6fb555ce0ed734e2d7b1c3acd018a2cff", "sha256:ff836cd4041e16003549449cc0a5e372f6b6f871eb89007ab0ee18fb2800fded" ], - "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2'", "version": "==3.19.2" }, "simplekv": { @@ -2787,7 +2785,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snowballstemmer": { @@ -3148,7 +3146,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" }, "uwsgi": { @@ -3287,20 +3285,12 @@ ], "version": "==0.12.0" }, - "zammad-py": { - "hashes": [ - "sha256:59af11dec42b4edf16ea4d538cc3fa07bc47cf7efebdca214d21c6c5bf4a6e2f", - "sha256:acd770cf98721536ccc88de9461cf8be04e1c30f8def400ae8825f0e8ebe6a72" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", - "version": "==3.0.0" - }, "zenodo-legacy": { - "editable": "True", + "editable": true, "path": "./legacy" }, "zenodo-rdm": { - "editable": "True", + "editable": true, "path": "./site" }, "zipp": { @@ -3629,7 +3619,6 @@ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], "index": "pypi", - "markers": "python_full_version >= '3.8.0'", "version": "==5.13.2" }, "itsdangerous": { @@ -3807,7 +3796,6 @@ "sha256:1d339b004f764d6cd0f06e690f6dd748df3d62e6fe1a692d6a5500ac2c5b75a5" ], "index": "pypi", - "markers": "python_version >= '2.7'", "version": "==0.3.12" }, "pytest-cov": { @@ -3832,7 +3820,6 @@ "sha256:eda7d69c20d6b7d58f0415e80473ec3f7d1604f00368f46437290051dfdbe216" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.1.7" }, "pytest-isort": { @@ -3948,7 +3935,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "tomli": { @@ -3972,7 +3959,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" }, "werkzeug": { diff --git a/site/setup.cfg b/site/setup.cfg index 3a3c02aa..ad7eb426 100644 --- a/site/setup.cfg +++ b/site/setup.cfg @@ -23,7 +23,6 @@ install_requires = nameparser>=1.1.1 pyinstrument>=4.5.1 sqltap>=0.3.11 - zammad-py>=3.0.0,<4.0.0 # TODO: Add once we fix PyPI package issues # invenio-swh>=0.2.0,<1.0.0 diff --git a/site/tests/conftest.py b/site/tests/conftest.py index 674c8327..ff0285ab 100644 --- a/site/tests/conftest.py +++ b/site/tests/conftest.py @@ -79,7 +79,6 @@ def app_config(app_config): } # OpenAIRE configs app_config["OPENAIRE_PORTAL_URL"] = "https://explore.openaire.eu" - app_config["SUPPORT_ZAMMAD_HTTPTOKEN"] = "changeme" return app_config diff --git a/site/zenodo_rdm/config.py b/site/zenodo_rdm/config.py index 021a3826..3038c5a7 100644 --- a/site/zenodo_rdm/config.py +++ b/site/zenodo_rdm/config.py @@ -31,116 +31,132 @@ # I18N_TRANSLATIONS_PATHS = [os.path.abspath("./site/zenodo_rdm/translations")] -# API endpoint to Zammad instance -SUPPORT_ZAMMAD_ENDPOINT = "http://localhost:8080/api/v1" +# Email address of sender. +SUPPORT_SENDER_EMAIL = "info@zenodo.org" -# Zammad token -SUPPORT_ZAMMAD_HTTPTOKEN = "" +# Name of the sender +SUPPORT_SENDER_NAME = "Zenodo Support" + +# Support emails +SUPPORT_EMAILS = ["info@zenodo.org"] + +# Support signature +SUPPORT_SIGNATURE = "https://zenodo-rdm.web.cern.ch/" # Support form categories SUPPORT_ISSUE_CATEGORIES = [ - { - "key": "general-inquiry", - "title": "General inquiry", - "description": "", - }, - { - "key": "problem-report", - "title": "Bug or problem", - "description": "Please provide direct links to pages and screenshots if possible. Include the error identifier if shown.", - }, { "key": "file-modification", "title": "File modification", "description": ( - "File modifications are possible within a short grace period after publishing:" - "" - "Please provide the following information:" - "" + "All requests related to updating files in already published " + "record(s). This includes new file addition, file removal or " + "file replacement. " + "Before sending a request, please consider creating a " + 'new version ' + "of your upload. Please first consult our " + 'FAQ to get familiar' + " with the file update conditions, to see if your case is " + "eligible.

" + "You request has to contain all of the points below:" + "
    " + "
  1. Provide a justification for the file change in the " + "description.
  2. " + "
  3. Mention any use of the record(s) DOI in publications or " + "online, e.g.: list papers that cite your record and " + "provide links to posts on blogs and social media. " + "Otherwise, state that to the best of your knowledge the DOI has " + "not been used anywhere.
  4. " + "
  5. Specify the record(s) you want to update by the Zenodo" + ' URL, e.g.: "https://zenodo.org/record/8428".
    ' + "Providing only the record's title, publication date or a " + "screenshot with search result is not explicit enough.
  6. " + "
  7. If you want to delete or update a file, specify it " + "by its filename, and mention if you want the name to " + "remain as is or changed (by default the filename of the new " + "file will be used).
  8. " + "
  9. Upload the new files below or provide a publicly-accessible " + "URL(s) with the files in the description.
  10. " + "
" + "Not providing full information on any of the points above " + "will significantly slow down your request resolution, " + "since our support staff will have to reply back with a request " + "for missing information." ), }, { - "key": "quota-increase", - "title": "Quota increase", + "key": "upload-quota", + "title": "File upload quota increase", "description": ( - "

We exceptionally grant a one-time quota increase up to 200GB. Requests beyond the 200GB are declined. Zenodo allows maximum 100 files in a record (this limit cannot be increased).

" - '

Before you send the request, please follow the actions described on https://help.zenodo.org/docs/deposit/manage-files/quota-increase/.

' + "All requests for a quota increase beyond the 50GB limit. " + "Please include the following information with your request:" + "
    " + "
  1. The total size of your dataset, number of files and the " + "largest file in the dataset. When referring to file sizes" + ' use ' + "SI units
  2. " + "
  3. Information related to the organization, project or grant " + "which was involved in the research, which produced the " + "dataset.
  4. " + "
  5. Information on the currently in-review or future papers that " + "will cite this dataset (if applicable). If possible specify the " + "journal or conference.
  6. " + "
" ), }, { - "key": "record-deletion", - "title": "Record deletion", + "key": "record-inactivation", + "title": "Record inactivation", "description": ( - "

The request must be made by the uploader (if you're not the uploader, choose take-down notice instead).

" - "

Record deletion is possible within a short grace period after publishing:

" - "" - "

Please provide:

" - "" + "Requests related to record inactivation, either by the record " + "owner or a third party. Please specify the record(s) in question " + "by the URL(s), and reason for the inactivation." ), }, { - "key": "user-deletion", - "title": "User deletion", + "key": "openaire", + "title": "OpenAIRE", "description": ( - "Please make sure you log in before you send the request. If you have uploaded any records or created any communities, please specify to who these should be transferred." + "All questions related to OpenAIRE reporting and grants. " + "Before sending a request, make sure your problem was not " + "already resolved, see OpenAIRE " + 'FAQ. ' + "For questions unrelated to Zenodo, you should contact OpenAIRE " + '' + "helpdesk directly." ), }, { - "key": "ownership-transfer", - "title": "Ownership transfer", - "description": "", - }, - { - "key": "personal-data-report", - "title": "Report personal data exposure", + "key": "partnership", + "title": "Partnership, outreach and media", "description": ( - "

Please provide the following information:

" - "" + "All questions related to possible partnerships, outreach, " + "invited talks and other official inquiries by media." + "If you are a journal, organization or conference organizer " + "interested in using Zenodo as archive for your papers, software " + "or data, please provide details for your usecase." ), }, { - "key": "security-report", - "title": "Report a security issue", - "description": "Please provide as detailed information as possible.", - }, - { - "key": "spam-report", - "title": "Report spam", + "key": "tech-support", + "title": "Security issue, bug or spam report", "description": ( - "

Please provide the following information:

" - "" + "Report a technical issue or a spam content on Zenodo." + "Please provide details on how to reproduce the bug. " + "Upload any screenshots or files which are relevant to the issue " + "or to means of reproducing it. Include error messages and " + "error codes you might be getting in the description.
" + "For REST API errors, provide a minimal code which produces the " + "issues. Use external services for scripts and long text" + ', e.g.: GitHub Gist. ' + "Do not disclose your password or REST API access tokens." + "" ), }, { - "key": "feature-request", - "title": "Feedback/Feature request", - "description": "", - }, - { - "key": "take-down", - "title": "Take-down notice", - "description": ( - "Please provide a direct link to the record or community to request us to take down. Please specify the reason for the take-down (e.g. copyright infringement, plagiarism, fraud, or similar)." - ), + "key": "other", + "title": "Other", + "description": ("Questions which do not fit into any other category."), }, ] @@ -156,6 +172,18 @@ # Support url endpoint SUPPORT_ENDPOINT = "/support" +#: Email body template. +SUPPORT_EMAIL_BODY_TEMPLATE = "zenodo_rdm/email_body.html" + +#: Email title template. +SUPPORT_EMAIL_TITLE_TEMPLATE = "zenodo_rdm/email_title.html" + +#: Email body template. +SUPPORT_EMAIL_CONFIRM_BODY_TEMPLATE = "zenodo_rdm/email_confirm_body.html" + +#: Email title template. +SUPPORT_EMAIL_CONFIRM_TITLE_TEMPLATE = "zenodo_rdm/email_confirm_title.html" + # Search query of recent uploads # Defaults to newest records search ZENODO_FRONTPAGE_RECENT_UPLOADS_QUERY = "type:(dataset OR software OR poster OR presentation) AND _exists_:parent.communities AND access.files:public" diff --git a/site/zenodo_rdm/support/email.py b/site/zenodo_rdm/support/email.py new file mode 100644 index 00000000..ca7484b7 --- /dev/null +++ b/site/zenodo_rdm/support/email.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 CERN. +# +# ZenodoRDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. +"""Implements an email service dedicated to the support views of ZenodoRDM.""" + +from flask import current_app, request +from flask_login import current_user +from flask_mail import Message +from invenio_accounts.sessions import _extract_info_from_useragent + +from .utils import format_user_email, render_template_to_string + + +class EmailService(object): + """Basic service to send an e-mail using the configured mail extension.""" + + @property + def mail(self): + """Retrieves the current mail extension. + + Throws an exception if none is found. + """ + service = current_app.extensions["mail"] + if not service: + raise Exception("No mail extension provided.") + return service + + def send_email( + self, title, recipients, sender, body, reply_to="", attachments=None + ): + """Sends an e-mail to the given recipient(s).""" + if type(recipients) != list: + recipients = [recipients] + + msg = Message( + title, sender=sender, recipients=recipients, reply_to=reply_to, body=body + ) + if attachments: + for upload in attachments: + msg.attach(upload.filename, "application/octet-stream", upload.read()) + self.mail.send(msg) + + +class SupportEmailService(EmailService): + """Provides an interface to send support related emails.""" + + def __init__(self): + """Constructor.""" + super().__init__() + + def send_support_email( + self, + sender_name, + email, + description, + send_user_agent, + subject, + category, + files, + title, + ): + """Sends an email to the configured support.""" + user_agent = _extract_info_from_useragent(request.headers.get("User-Agent")) + context = { + "user_id": current_user.get_id(), + "name": sender_name, + "email": email, + "description": description, + "user_agent": user_agent if send_user_agent else None, + "subject": subject, + "category": category, + } + msg_body = render_template_to_string(self.support_email_body_template, context) + msg_title = title + sender = format_user_email(email, sender_name) + recipients = self.support_emails + reply_to = email + return self.send_email(msg_title, recipients, sender, msg_body, reply_to, files) + + # TODO aggregate in a single method and just pass things + def send_confirmation_email(self, recipients): + """Sends a confirmation e-mail to the user.""" + context = { + "support_name": self.support_sender_name, + "support_signature": current_app.config["SUPPORT_SIGNATURE"], + } + sender = format_user_email(self.support_sender_email, self.support_sender_name) + msg_body = render_template_to_string( + self.support_email_confirm_body_template, context + ) + msg_title = self.support_sender_name + reply_to = self.support_sender_email + return self.send_email(msg_title, recipients, sender, msg_body, reply_to) + + @property + def support_sender_email(self): + """Support sender e-mail.""" + return current_app.config["SUPPORT_SENDER_EMAIL"] + + @property + def support_sender_name(self): + """Support sender name.""" + return current_app.config["SUPPORT_SENDER_NAME"] + + @property + def support_email_body_template(self): + """Support email body template.""" + return current_app.config["SUPPORT_EMAIL_BODY_TEMPLATE"] + + @property + def support_email_confirm_title_template(self): + """Support confirmation email title.""" + return current_app.config["SUPPORT_EMAIL_CONFIRM_TITLE_TEMPLATE"] + + @property + def support_email_confirm_body_template(self): + """Support confirmation email body.""" + return current_app.config["SUPPORT_EMAIL_CONFIRM_BODY_TEMPLATE"] + + @property + def support_emails(self): + """List of support emails.""" + return current_app.config["SUPPORT_EMAILS"] diff --git a/site/zenodo_rdm/support/errors.py b/site/zenodo_rdm/support/errors.py new file mode 100644 index 00000000..954c5f58 --- /dev/null +++ b/site/zenodo_rdm/support/errors.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 CERN. +# +# ZenodoRDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. +"""ZenodoRDM support error classes.""" + +from ..errors import ZenodoRDMError + + +class ConfirmationEmailNotSent(ZenodoRDMError): + """Implements an error when a support confirmation e-mail was not sent.""" + + def __init__(self, message): + """Initialise error.""" + super().__init__("Confirmation email failed to send. Error: {}".format(message)) + + +class SupportEmailNotSent(ZenodoRDMError): + """Implements an error when a support e-mail was not sent.""" + + def __init__(self, message): + """Initialise error.""" + super().__init__("Support email failed to send. Error: {}".format(message)) diff --git a/site/zenodo_rdm/support/schema.py b/site/zenodo_rdm/support/schema.py index 0bf9e54f..4c26a132 100644 --- a/site/zenodo_rdm/support/schema.py +++ b/site/zenodo_rdm/support/schema.py @@ -8,7 +8,7 @@ from flask import current_app from marshmallow import RAISE, Schema, ValidationError, fields, validate, validates -from marshmallow_utils.fields import SanitizedUnicode +from marshmallow_utils.fields import SanitizedHTML from werkzeug.local import LocalProxy from werkzeug.utils import cached_property @@ -33,7 +33,7 @@ class Meta: name = fields.String(required=True) email = fields.String(required=True, validate=validate.Email()) - description = SanitizedUnicode(required=True) + description = SanitizedHTML(required=True) subject = fields.String(required=True) category = fields.String(required=True) sysInfo = fields.Boolean() diff --git a/site/zenodo_rdm/support/support.py b/site/zenodo_rdm/support/support.py index 3a92226c..b01f7f47 100644 --- a/site/zenodo_rdm/support/support.py +++ b/site/zenodo_rdm/support/support.py @@ -4,22 +4,18 @@ # # ZenodoRDM is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. - """Implements the support view for ZenodoRDM.""" -import mimetypes -from base64 import b64encode +import smtplib from collections import OrderedDict -from flask import current_app, flash, redirect, render_template, request, url_for +from flask import current_app, redirect, render_template, request, url_for from flask.views import MethodView -from flask_login import current_user from invenio_accounts.sessions import _extract_info_from_useragent -from invenio_i18n import _ -from requests.exceptions import RequestException from werkzeug.utils import cached_property -from zammad_py import ZammadAPI +from .email import SupportEmailService +from .errors import ConfirmationEmailNotSent, SupportEmailNotSent from .schema import SupportFormSchema @@ -29,11 +25,8 @@ class ZenodoSupport(MethodView): def __init__(self): """Constructor.""" self.template = "zenodo_rdm/support.html" + self.email_service = SupportEmailService() self.support_form_schema = SupportFormSchema() - self.client = ZammadAPI( - url=current_app.config["SUPPORT_ZAMMAD_ENDPOINT"], - http_token=current_app.config["SUPPORT_ZAMMAD_HTTPTOKEN"], - ) def get(self): """Renders the support template.""" @@ -43,6 +36,7 @@ def get(self): browser_string = browser_client + " " + browser_version platform = user_agent.get("os", "") system_info = {"browser": browser_string, "platform": platform} + return render_template( self.template, categories=self.categories, @@ -53,137 +47,65 @@ def post(self): """Receives a form, validates its data and handles it.""" input_data = {**request.form.to_dict(), "files": request.files.getlist("files")} data = self.validate_form(input_data) - try: - self.handle_form(data) - except RequestException as e: - # Any error from Zammad being down to badly formatted requests. - raise Exception( - "The request could not be sent to the support system due to an internal error." - ) + self.handle_form(data) + return redirect(url_for("invenio_app_rdm.frontpage_view_function")) + def handle_form(self, form_data): + """Form controller.""" + self.send_support_email(form_data) + self.send_confirmation_email(form_data) + def validate_form(self, form_data): """Validates form using a schema.""" return self.support_form_schema.load(form_data) - def handle_form(self, data): - """Form controller.""" - customer_id = self.handle_customer(data["email"], data["name"]) - - params = { - "title": data["subject"], - "group": "Support (1st line)", - "customer_id": customer_id, - "sender": "Customer", - "type": data["category"], - "article": { - "subject": data["subject"], - "body": data["description"], - "content_type": "text/plain", - "type": "web", - "internal": False, - "sender": "Customer", - "origin_by_id": customer_id, - }, - } - if "files" in data: - params["article"]["attachments"] = [] - # Limit to max 20 files - for f in data["files"][:20]: - params["article"]["attachments"].append( - { - "filename": f.filename, - "data": b64encode(f.stream.read()), - "mime-type": mimetypes.guess_type(f.filename)[0] - or "application/octet-stream", - } + def send_support_email(self, data): + """Send an email to support.""" + try: + self.email_service.send_support_email( + sender_name=data.get("name"), + email=data.get("email"), + description=data.get("description"), + send_user_agent=data.get("sysInfo"), + subject=data.get("subject"), + category=data.get("category"), + files=data.get("files"), + title="[{}]: {}".format(data.get("category"), data.get("subject")), + ) + except smtplib.SMTPSenderRefused as e: + raise SupportEmailNotSent( + "There was an issue sending an email to the provided " + "address (ours), please make sure it is correct. " + "If this issue persists you can send " + "us an email directly to {}".format(self.email_service.support_emails) + ) + except Exception as e: + raise SupportEmailNotSent( + "There was an issue sending the support request." + "If this issue persists send us an email directly to {}".format( + self.email_service.support_emails ) - - # Browser info is added as a note. - if data["sysInfo"]: - user_agent = _extract_info_from_useragent(request.headers.get("User-Agent")) - browser_client = user_agent.get("browser", "") - browser_version = user_agent.get("browser_version", "") - browser_string = browser_client + " " + browser_version - platform = user_agent.get("os", "") - params["article"]["body"] += f"\n\nBrowser: {browser_string} OS: {platform}" - - new_ticket = self.client.ticket.create(params=params) - flash( - _( - "You support request #%(ticket_id)s was created. You will receive a confirmation email shortly." ) - % {"ticket_id": new_ticket["id"]}, - "info", - ) - def find_customer(self, email): - """Find a customer.""" - customer = None - page = self.client.user.search(f"email:{email}") - for u in page: - if u["email"] == email: - customer = u - break - return customer - - def split_name(self, name): - """Split name into given and family name.""" - parts = name.strip().split(" ") - if len(parts) == 1: - return name.strip(), "" - elif len(parts) > 1: - return parts[0], " ".join(parts[1:]).strip() - else: - return "", "" - - def create_customer(self, email, name, zenodo_user_id): - """Create a customer.""" - params = { - "email": email, - "roles": ["Customer"], - } - if name: - first_name, last_name = self.split_name(name) - params["firstname"] = first_name - params["lastname"] = last_name - if zenodo_user_id is not None: - params["zenodo_user"] = zenodo_user_id - return self.client.user.create(params=params) - - def update_customer(self, customer, email, name, zenodo_user_id): - """Update a customer.""" - params = {} - if zenodo_user_id: - if customer.get("zenodo_user", None) != zenodo_user_id: - params["zenodo_user"] = zenodo_user_id - if name: - firstname, lastname = self.split_name(name) - if customer["firstname"] != firstname: - params["firstname"] = firstname - if customer["lastname"] != lastname: - params["lastname"] = lastname - if params: - customer = self.client.user.update(customer["id"], params=params) - return customer - - def handle_customer(self, email, name): - """Create the customer in Zammad.""" - # Set initial parameters - zenodo_user_id = None - if current_user.is_authenticated: - email = current_user.email - name = current_user.user_profile.get("full_name", "") - zenodo_user_id = current_user.get_id() - email = email.lower() - # TODO: Create or update organisation if not already in Zammad. - customer = self.find_customer(email) - if customer is None: - customer = self.create_customer(email, name, zenodo_user_id) - else: - self.update_customer(customer, email, name, zenodo_user_id) - - return customer["id"] + def send_confirmation_email(self, data): + """Send a confirmation email to the user.""" + try: + self.email_service.send_confirmation_email(recipients=data.get("email")) + except smtplib.SMTPSenderRefused as e: + raise ConfirmationEmailNotSent( + "There was an issue sending a confirmation email to the provided " + "address (yours), please make sure it is correct. " + "If this issue persists you can send " + "us an email directly to {}".format(self.email_service.support_emails) + ) + except Exception as e: + raise ConfirmationEmailNotSent( + "There was an issue sending the confirmation email." + "If this issue persists send us an email directly to {}".format( + self.email_service.support_emails + ) + ) @cached_property def categories(self): @@ -191,3 +113,8 @@ def categories(self): return OrderedDict( (c["key"], c) for c in current_app.config["SUPPORT_ISSUE_CATEGORIES"] ) + + @cached_property + def support_emails(self): + """List of support emails.""" + return current_app.config["SUPPORT_EMAILS"] diff --git a/site/zenodo_rdm/support/utils.py b/site/zenodo_rdm/support/utils.py new file mode 100644 index 00000000..f3d11b27 --- /dev/null +++ b/site/zenodo_rdm/support/utils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 CERN. +# +# ZenodoRDM is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. +"""Utility functions for the ZenodoRDM support views.""" + +from flask import current_app + + +def render_template_to_string(template, context): + """Render a Jinja template with the given context.""" + template = current_app.jinja_env.get_or_select_template(template) + return template.render(context) + + +def format_user_email(email, name): + """Format the user's email as 'Full Name ' or 'email'.""" + if name: + email = "{name} <{email}>".format(name=name, email=email) + return email diff --git a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_body.html b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_body.html new file mode 100644 index 00000000..9dbb8b32 --- /dev/null +++ b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_body.html @@ -0,0 +1,35 @@ +{# +# This file is part of Zenodo. +# Copyright (C) 2022 CERN. +# +# Zenodo is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Zenodo is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Zenodo; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. +-#} + +From: {{ name }} <{{ email }}> {% if user_id %}({{user_id}}){% endif %} + +{%- if user_agent %} +Operating System: {{ user_agent.os }} +Browser: {{ user_agent.browser }} +Device: {{ user_agent.device }} +{%- else %} +User's browser information not included. +{%- endif %} + +{{ description | safe }} diff --git a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_confirm_body.html b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_confirm_body.html new file mode 100644 index 00000000..e5d1660c --- /dev/null +++ b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/email_confirm_body.html @@ -0,0 +1,32 @@ +{# +# This file is part of Zenodo. +# Copyright (C) 2022 CERN. +# +# Zenodo is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Zenodo is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Zenodo; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. +-#} + +Thank you for contacting {{ support_name }}. + +We have received your message, and we will do our best to get back to you as soon as possible. +This is an automated confirmation of your request, please do not reply to this email. + +{{ support_name }} +{{ support_signature }} + diff --git a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/support.html b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/support.html index a600d125..7a0799af 100644 --- a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/support.html +++ b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/support.html @@ -22,93 +22,16 @@
-

Support

- -
- -
-
- Before you contact us -
-

We appreciate that you search for answers to your questions in below linked documentation before you reach out to us.

-
-
- -
-
-
- -
- Answers to all of the most frequently asked questions on our support line including e.g. -
    -
  • Account management
  • -
  • Creating and managing records
  • -
  • Review processes
  • -
  • Safe listing and usage statistics
  • -
  • Policy questions
  • -
-
-
-
-
-
- -
- Easy to follow step-by-step instructions for how to get started and use both easy and advanced Zenodo features including: -
    -
  • Uploading and managing records
  • -
  • Collaborating and sharing
  • -
  • Communities and membership management
  • -
  • Account and profiles
  • -
-
-
-
-
-
- -
- General information about Zenodo, our infrastructure, projects, and policies including: - -
-
-
-
-
-
Open science (by OpenAIRE)
-
- OpenAIRE provides general guides/training on open science, Horizon Europe, research data management (RDM) and legal issues for researchers. - -
-
-
-
- - +

Contact us

+

Before making a request, you can have a look at our FAQ and other resources for more detailed information about the operation, features and goals of Zenodo.

+ {%- if not user %} +

It is recommended to login before sending a request, so we can automatically fill-in your contact information. It is especially important for quicker resolution of all technical issues and requests concerning your Zenodo account.

+ {%- endif %}
-

Contact us

- {%- if not is_user_authenticated %} -
-
- Log in -
- Log in -

Please log into Zenodo if you have already have an account by clicking the button. This helps us resolve your requests faster and is required for certain types of requests.

-
- {%- endif %} {% block root_section %}
{% endblock %} @@ -137,4 +60,4 @@

Contact us

{% block javascript %} {{ super() }} {{ webpack['zenodo-rdm-support.js'] }} -{% endblock %} +{% endblock %} \ No newline at end of file