From 78f6de187a5dc16b2f2e76940b21c734e629212a Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Wed, 15 Jan 2025 19:17:07 -0500 Subject: [PATCH 1/3] Add report for deleted Guidebook items We use the tracking table to grab all items that were synced to guidebook but have since been deleted, and send that out in an email. We piggyback on the Guidebook updates email when at-con and check every twelve hours otherwise. Also turns these emails off by default -- to enable them, you need to define the email address they get sent to. --- uber/configspec.ini | 4 ++ uber/tasks/panels.py | 71 +++++++++++++++++++-- uber/templates/emails/guidebook_deletes.txt | 7 ++ uber/templates/emails/guidebook_updates.txt | 11 +++- 4 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 uber/templates/emails/guidebook_deletes.txt diff --git a/uber/configspec.ini b/uber/configspec.ini index 99182f21c..8df2f6409 100644 --- a/uber/configspec.ini +++ b/uber/configspec.ini @@ -737,6 +737,10 @@ panels_confirm_deadline = integer(default=0) # be ready, so we just set this to false whenever it is. hide_schedule = boolean(default=True) +# Emails about changes to schedule items and other Guidebook items are sent TO this address. +# If the address is not set, these emails are disabled. +guidebook_updates_email = string(default="") + # These are the areas from which we'll show events to associated with panel # applications on the schedule. panel_rooms = string_list(default=list()) diff --git a/uber/tasks/panels.py b/uber/tasks/panels.py index 110f21325..1a7ad65cf 100644 --- a/uber/tasks/panels.py +++ b/uber/tasks/panels.py @@ -1,16 +1,47 @@ +import json + +from collections import defaultdict from datetime import timedelta from dateutil import parser as dateparser from sqlalchemy import or_ from uber.config import c from uber.decorators import render -from uber.models import Session +from uber.models import Email, Session, Tracking from uber.tasks import celery from uber.tasks.email import send_email from uber.utils import GuidebookUtils, localized_now -__all__ = ['panels_waitlist_unaccepted_panels', 'sync_guidebook_models', 'check_stale_guidebook_models'] +__all__ = ['panels_waitlist_unaccepted_panels', 'sync_guidebook_models', + 'check_deleted_guidebook_models', 'check_stale_guidebook_models'] + + +def _get_deleted_models(session, deleted_since=None): + deleted_synced = session.query(Tracking).filter(Tracking.action == c.DELETED, + Tracking.snapshot.contains('"last_synced": {"data": {"guidebook"')) + if deleted_since: + deleted_synced = deleted_synced.filter(Tracking.when > deleted_since) + + deleted_models = defaultdict(list) + model_names = {} + + for key, label in c.GUIDEBOOK_MODELS: + model_names[key] = label + + for tracking_entry in deleted_synced: + snapshot = json.loads(tracking_entry.snapshot) + + model = snapshot['_model'] + if model == 'GuestGroup': + model += '_band' if snapshot['group_type'] == c.BAND else '_guest' + elif model == 'Group': + model += '_dealer' + + model_name = 'Schedule Item' if model == 'Event' else model_names[model] + + deleted_models[model_name].append(snapshot['last_synced']['data']['guidebook']['name']) + return deleted_models @celery.task @@ -31,9 +62,30 @@ def sync_guidebook_models(selected_model, sync_time, id_list): session.commit() +@celery.schedule(timedelta(hours=12)) +def check_deleted_guidebook_models(): + if not c.PRE_CON or not c.GUIDEBOOK_UPDATES_EMAIL: + return + + with Session() as session: + subject = f"Deleted Guidebook Items: {localized_now().strftime("%A %-I:%M %p")}" + last_email = session.query(Email).filter(Email.subject.contains("Deleted Guidebook Items") + ).order_by(Email.when.desc()).first() + + deleted_models = _get_deleted_models(session, deleted_since=last_email.when) if last_email else _get_deleted_models(session) + + if deleted_models: + body = render('emails/guidebook_deletes.txt', { + 'deleted_models': deleted_models, + }, encoding=None) + send_email.delay(c.REPORTS_EMAIL, c.GUIDEBOOK_UPDATES_EMAIL, + subject, body, ident="guidebook_deletes" + ) + + @celery.schedule(timedelta(minutes=15)) def check_stale_guidebook_models(): - if not c.AT_THE_CON: + if not c.AT_THE_CON or not c.GUIDEBOOK_UPDATES_EMAIL: return with Session() as session: @@ -41,11 +93,20 @@ def check_stale_guidebook_models(): stale_models = [key for key in cl_updates if cl_updates[key]] if schedule_updates: stale_models.append('Schedule') - if stale_models: + + last_email = session.query(Email).filter(or_( + Email.subject.contains("Guidebook Updates"), + Email.subject.contains("Deleted Guidebook Items")) + ).order_by(Email.when.desc()).first() + + deleted_models = _get_deleted_models(session, deleted_since=last_email.when) if last_email else _get_deleted_models(session) + + if stale_models or deleted_models: body = render('emails/guidebook_updates.txt', { 'stale_models': stale_models, + 'deleted_models': deleted_models, }, encoding=None) - send_email.delay(c.REPORTS_EMAIL, "gb-ops@magfest.org", + send_email.delay(c.REPORTS_EMAIL, c.GUIDEBOOK_UPDATES_EMAIL, f"Guidebook Updates: {localized_now().strftime("%A %-I:%M %p")}", body, ident="guidebook_updates" ) diff --git a/uber/templates/emails/guidebook_deletes.txt b/uber/templates/emails/guidebook_deletes.txt new file mode 100644 index 000000000..d0ef08342 --- /dev/null +++ b/uber/templates/emails/guidebook_deletes.txt @@ -0,0 +1,7 @@ +Attention Guidebook admins! The items below have been deleted from the system. + +{% for category in deleted_models %}{{ category }}(s): +{% for item in deleted_models[category] %} - {{ item }} +{% endfor %}{% endfor %} + +This email will be the only record of these item deletions. Please keep it until you delete all the above items from Guidebook. \ No newline at end of file diff --git a/uber/templates/emails/guidebook_updates.txt b/uber/templates/emails/guidebook_updates.txt index 66df1154b..364e2adde 100644 --- a/uber/templates/emails/guidebook_updates.txt +++ b/uber/templates/emails/guidebook_updates.txt @@ -1,6 +1,13 @@ Attention Guidebook admins! -The following categories have stale items: + +{% if stale_models %}The following categories have been updated in the system: {% for label in stale_models %}- {{ label }} {% endfor %} -You can review all pending items here: {{ c.URL_BASE }}/schedule_reports/index \ No newline at end of file +{% endif %}{% if deleted_models %}The following items have been deleted: +{% for category in deleted_models %}{{ category }}(s): +{% for item in deleted_models[category] %} - {{ item }} +{% endfor %} +{% endfor %} + +{% endif %}You can review all pending items here: {{ c.URL_BASE }}/schedule_reports/index \ No newline at end of file From 033ffec7ecc6703a3dfb5ef250ad5a9a4c3cba00 Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Wed, 15 Jan 2025 19:28:04 -0500 Subject: [PATCH 2/3] Fix AJAX not working on schedule updates page This is another example of the issue where DataTables pagination breaks jQuery selectors, and we're fixing it the same way. --- uber/templates/schedule_reports/index.html | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/uber/templates/schedule_reports/index.html b/uber/templates/schedule_reports/index.html index 69c11e7da..454f8eb00 100644 --- a/uber/templates/schedule_reports/index.html +++ b/uber/templates/schedule_reports/index.html @@ -43,9 +43,9 @@ }); } - let markItemSynced = function(event){ - let form = $(this); - event.preventDefault(); + let markItemSynced = function(){ + let form = $(this).closest('form'); + $.ajax({ method: 'POST', url: 'mark_item_synced', @@ -81,7 +81,10 @@ } $().ready(function () { - $(".sync-item").submit(markItemSynced); + $('#schedule-table').on('click', '.sync-item', markItemSynced); + {% for model, label in c.GUIDEBOOK_MODELS %} + $('#{{ model }}-table').on('click', '.sync-item', markItemSynced); + {% endfor %} }); {% set now = now() %} @@ -158,7 +161,7 @@

Guidebook Exports and Updates -- {{ now|datetime_local("%m/%d/%Y, %-I:%M%p")

Items in italics has been changed since the last sync.

- +
@@ -203,13 +206,13 @@

Guidebook Exports and Updates -- {{ now|datetime_local("%m/%d/%Y, %-I:%M%p") {% if c.HAS_SCHEDULE_ACCESS %} View {% endif %} -
+ {{ csrf_token() }} - + @@ -234,7 +237,7 @@

Guidebook Exports and Updates -- {{ now|datetime_local("%m/%d/%Y, %-I:%M%p")

Items in italics has been changed since the last sync.

-

Name
+
{% for key, label in c.GUIDEBOOK_PROPERTIES %} @@ -275,13 +278,13 @@

Guidebook Exports and Updates -- {{ now|datetime_local("%m/%d/%Y, %-I:%M%p") {% if result.guidebook_edit_link.split('/')[1] in (c.GETTABLE_SITE_PAGES[0] + c.ADMIN_ACCESS_SET|list) %} View {% endif %} -
+ {{ csrf_token() }} - + From 35820777eafc085089773dae49c1bc68a5681020 Mon Sep 17 00:00:00 2001 From: Victoria Earl Date: Wed, 15 Jan 2025 19:33:10 -0500 Subject: [PATCH 3/3] Show changes when events' panels are changed We were keeping events synced with their panels, but they weren't showing up in the schedule changes list because we weren't also synced the last_updated value. This should fix that! --- uber/models/panels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uber/models/panels.py b/uber/models/panels.py index a9b9a4872..7544aa84e 100644 --- a/uber/models/panels.py +++ b/uber/models/panels.py @@ -149,11 +149,13 @@ class PanelApplication(MagModel): @presave_adjustment def update_event_info(self): - if self.event: + if self.event and any([getattr(self.event, key, '') != getattr(self, key, '') for key in [ + 'name', 'description', 'public_description', 'track']]): self.event.name = self.name self.event.description = self.description self.event.public_description = self.public_description self.event.track = self.track + self.event.last_updated = self.last_updated @presave_adjustment def set_default_dept(self):