From 1691c9f41db4ccbd3e0c3098951da4de37e4c2b9 Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Wed, 11 Oct 2023 11:12:36 +0530 Subject: [PATCH] Update rsvp registered email schema to automatically add event to google calendar (#1895) * schema for adding to google calendar * Use project.location * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use single quotes, fix venue type * Use Rsvp for EventRegistration schema --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kiran Jonnalagadda --- funnel/models/rsvp.py | 4 +- funnel/transports/email/send.py | 86 +++++++++++++++---- .../views/notifications/rsvp_notification.py | 6 +- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py index a5bd82361..ac6390686 100644 --- a/funnel/models/rsvp.py +++ b/funnel/models/rsvp.py @@ -45,7 +45,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model): project_id = sa.orm.mapped_column( sa.Integer, sa.ForeignKey('project.id'), nullable=False, primary_key=True ) - project = with_roles( + project: Mapped[Project] = with_roles( relationship(Project, backref=backref('rsvps', cascade='all', lazy='dynamic')), read={'owner', 'project_promoter'}, grants_via={None: project_child_role_map}, @@ -54,7 +54,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model): participant_id: Mapped[int] = sa.orm.mapped_column( sa.ForeignKey('account.id'), nullable=False, primary_key=True ) - participant = with_roles( + participant: Mapped[Account] = with_roles( relationship(Account, backref=backref('rsvps', cascade='all', lazy='dynamic')), read={'owner', 'project_promoter'}, grants={'owner'}, diff --git a/funnel/transports/email/send.py b/funnel/transports/email/send.py index 8ed054bb3..f31343c0c 100644 --- a/funnel/transports/email/send.py +++ b/funnel/transports/email/send.py @@ -17,12 +17,13 @@ from baseframe import _, statsd from ... import app -from ...models import Account, EmailAddress, EmailAddressBlockedError +from ...models import Account, EmailAddress, EmailAddressBlockedError, Rsvp from ..exc import TransportRecipientError __all__ = [ 'EmailAttachment', 'jsonld_confirm_action', + 'jsonld_event_reservation', 'jsonld_view_action', 'process_recipient', 'send_email', @@ -42,32 +43,85 @@ class EmailAttachment: def jsonld_view_action(description: str, url: str, title: str) -> dict[str, object]: + """Schema.org JSON-LD markup for an email view action.""" return { - "@context": "http://schema.org", - "@type": "EmailMessage", - "description": description, - "potentialAction": {"@type": "ViewAction", "name": title, "url": url}, - "publisher": { - "@type": "Organization", - "name": current_app.config['SITE_TITLE'], - "url": 'https://' + current_app.config['DEFAULT_DOMAIN'] + '/', + '@context': 'https://schema.org', + '@type': 'EmailMessage', + 'description': description, + 'potentialAction': {'@type': 'ViewAction', 'name': title, 'url': url}, + 'publisher': { + '@type': 'Organization', + 'name': current_app.config['SITE_TITLE'], + 'url': 'https://' + current_app.config['DEFAULT_DOMAIN'] + '/', }, } def jsonld_confirm_action(description: str, url: str, title: str) -> dict[str, object]: + """Schema.org JSON-LD markup for an email confirmation action.""" return { - "@context": "http://schema.org", - "@type": "EmailMessage", - "description": description, - "potentialAction": { - "@type": "ConfirmAction", - "name": title, - "handler": {"@type": "HttpActionHandler", "url": url}, + '@context': 'https://schema.org', + '@type': 'EmailMessage', + 'description': description, + 'potentialAction': { + '@type': 'ConfirmAction', + 'name': title, + 'handler': {'@type': 'HttpActionHandler', 'url': url}, }, } +def jsonld_event_reservation(rsvp: Rsvp) -> dict[str, object]: + """Schema.org JSON-LD markup for an event reservation.""" + location: str | dict[str, object] + venue = rsvp.project.primary_venue + if venue is not None: + location = { + '@type': 'Place', + 'name': venue.title, + } + if venue.address1: + postal_address = { + '@type': 'PostalAddress', + 'streetAddress': venue.address1, + 'addressLocality': venue.city, + 'addressRegion': venue.state, + 'postalCode': venue.postcode, + 'addressCountry': venue.country, + } + location['address'] = postal_address + else: + location = rsvp.project.location + return { + '@context': 'https://schema.org', + '@type': 'EventReservation', + 'reservationNumber': rsvp.uuid_b58, + 'reservationStatus': ( + 'https://schema.org/ReservationConfirmed' + if rsvp.state.YES + else 'https://schema.org/ReservationCancelled' + if rsvp.state.NO + else 'https://schema.org/ReservationPending' + ), + 'underName': { + '@type': 'Person', + 'name': rsvp.participant.fullname, + }, + 'reservationFor': { + '@type': 'Event', + 'name': rsvp.project.joined_title, + 'url': rsvp.project.absolute_url, + 'startDate': rsvp.project.start_at, + 'location': location, + 'performer': { + '@type': 'Organization', + 'name': rsvp.project.account.title, + }, + }, + 'numSeats': '1', + } + + def process_recipient(recipient: EmailRecipient) -> str: """ Process recipient in any of the given input formats. diff --git a/funnel/views/notifications/rsvp_notification.py b/funnel/views/notifications/rsvp_notification.py index 81fface2b..65fe69c24 100644 --- a/funnel/views/notifications/rsvp_notification.py +++ b/funnel/views/notifications/rsvp_notification.py @@ -120,11 +120,7 @@ def email_content(self): return render_template( 'notifications/rsvp_yes_email.html.jinja2', view=self, - jsonld=email.jsonld_view_action( - self.rsvp.project.joined_title, - self.rsvp.project.url_for(_external=True), - _("View project"), - ), + jsonld=email.jsonld_event_reservation(self.rsvp), ) def sms(