Skip to content

Commit 77d4d55

Browse files
committed
Push notifications for upcoming volunteer shifts
1 parent a8758cd commit 77d4d55

File tree

4 files changed

+129
-88
lines changed

4 files changed

+129
-88
lines changed

apps/base/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,5 @@ def deliveries():
207207
from . import tasks_banking # noqa
208208
from . import tasks_export # noqa
209209
from . import tasks_videos # noqa
210+
from ..notifications import tasks # noqa
210211
from . import dev # noqa

apps/notifications/jobs.py

Lines changed: 0 additions & 88 deletions
This file was deleted.

apps/notifications/tasks.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from sqlalchemy import and_
2+
from main import db
3+
from datetime import datetime, timedelta
4+
from flask import current_app as app
5+
6+
from models import scheduled_task
7+
from models.cfp import Proposal
8+
from models.user import User
9+
from models.volunteer.shift import Shift, ShiftEntry
10+
from models.web_push import PushNotificationJob, enqueue_if_not_exists
11+
from models.notifications import UserNotificationPreference
12+
from pywebpush import webpush, WebPushException
13+
14+
15+
def deliver_notification(job: PushNotificationJob):
16+
"""Deliver a push notification from a PushNotificationJob.
17+
18+
The passed job will be mutated to reflect delivery state. A job which isn't
19+
queued will be skipped over.
20+
"""
21+
if job.state != "queued":
22+
return
23+
24+
try:
25+
webpush(
26+
subscription_info=job.target.subscription_info,
27+
data=job.title,
28+
vapid_private_key=app.config["WEBPUSH_PRIVATE_KEY"],
29+
vapid_claims={
30+
"sub": "mailto:[email protected]",
31+
},
32+
)
33+
34+
job.state = "delivered"
35+
except WebPushException as err:
36+
job.state = "failed"
37+
job.error = err.message
38+
39+
40+
@scheduled_task(minutes=1)
41+
def send_queued_notifications():
42+
jobs = PushNotificationJob.query.where(
43+
PushNotificationJob.state == "queued"
44+
and (PushNotificationJob.not_before is None or PushNotificationJob.not_before <= datetime.now())
45+
).all()
46+
47+
for job in jobs:
48+
deliver_notification(job)
49+
db.session.add(job)
50+
51+
db.session.commit()
52+
53+
54+
@scheduled_task(minutes=15)
55+
def queue_content_notifications(time=None) -> None:
56+
if time is None:
57+
time = datetime.now()
58+
59+
users = User.query.join(
60+
UserNotificationPreference,
61+
User.notification_preferences.and_(UserNotificationPreference.favourited_content),
62+
)
63+
64+
upcoming_content = Proposal.query.filter(
65+
and_(Proposal.scheduled_time >= time, Proposal.scheduled_time <= time + timedelta(minutes=16))
66+
).all()
67+
68+
for user in users:
69+
user_favourites = [f.id for f in user.favourites]
70+
favourites = [p for p in upcoming_content if p.id in user_favourites]
71+
for proposal in favourites:
72+
for target in user.web_push_targets:
73+
enqueue_if_not_exists(
74+
target=target,
75+
related_to=f"favourite,user:{user.id},proposal:{proposal.id},target:{target.id}",
76+
title=f"{proposal.title} is happening soon at {proposal.scheduled_venue.name}",
77+
not_before=proposal.scheduled_time - timedelta(minutes=15),
78+
)
79+
80+
db.session.commit()
81+
82+
83+
@scheduled_task(minutes=15)
84+
def queue_shift_notifications(time=None) -> None:
85+
if time is None:
86+
time = datetime.now()
87+
88+
upcoming_shifts: list[Shift] = Shift.query.filter(
89+
and_(Shift.start >= time, Shift.start <= time + timedelta(minutes=16))
90+
).all()
91+
92+
for shift in upcoming_shifts:
93+
for user in shift.volunteers:
94+
if user.notification_preferences.volunteer_shifts:
95+
for target in user.web_push_targets:
96+
enqueue_if_not_exists(
97+
target=target,
98+
related_to=f"shift_reminder,user:{user.id},shift:{shift.id},target:{target.id}",
99+
title=f"Your {shift.role.name} shift is about to start, please go to {shift.venue.name}.",
100+
not_before=shift.start - timedelta(minutes=15),
101+
)
102+
103+
db.session.commit()

models/web_push.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ def notify(target, message):
2222
)
2323

2424

25+
def enqueue_if_not_exists(
26+
target: "WebPushTarget",
27+
title: str,
28+
body: str | None = None,
29+
related_to: str | None = None,
30+
not_before: datetime | None = None,
31+
) -> "PushNotificationJob":
32+
if related_to is not None:
33+
existing_job = PushNotificationJob.query.where(
34+
PushNotificationJob.related_to == related_to
35+
).one_or_none()
36+
if existing_job is not None:
37+
return existing_job
38+
39+
job = PushNotificationJob(
40+
target=target,
41+
title=title,
42+
body=body,
43+
related_to=related_to,
44+
not_before=not_before,
45+
)
46+
db.session.add(job)
47+
return job
48+
49+
2550
class WebPushTarget(BaseModel):
2651
__table_name__ = "web_push_target"
2752
id = db.Column(db.Integer, primary_key=True)

0 commit comments

Comments
 (0)