Skip to content

Commit 73f2fd1

Browse files
committed
feat: add push notifications
1 parent f164994 commit 73f2fd1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2138
-187
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,10 @@ package-lock.json
5656
# Virtual environments
5757
venv/
5858
.venv/
59+
60+
# Webpush
61+
/keys/webpush/
62+
/keys/
63+
64+
# Keys
65+
*.pem

Ion.egg-info/PKG-INFO

+4
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,14 @@ Requires-Dist: sphinx-bootstrap-theme==0.8.1
7474
Requires-Dist: tblib==1.7.0
7575
Requires-Dist: vine==5.0.0
7676
Requires-Dist: xhtml2pdf==0.2.11
77+
Requires-Dist: django-push-notifications[WP]==3.1.0
78+
Requires-Dist: py-vapid==1.9.1
79+
Requires-Dist: pywebpush==2.0.0
7780
Requires-Dist: asgiref>=3.3.4
7881
Requires-Dist: pillow>=9.0.0
7982
Requires-Dist: tinycss2
8083
Requires-Dist: twisted>=21.7.0
84+
Requires-Dist: python-bidi==0.4.2
8185

8286
**********
8387
Intranet 3

Ion.egg-info/SOURCES.txt

+20-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ config/scripts/README.md
4848
config/scripts/create_activities.py
4949
config/scripts/create_blocks.py
5050
config/scripts/create_users.py
51+
config/scripts/create_vapid_keys.py
5152
config/vagrant/devconfig.json.sample
5253
config/vagrant/ion_env_setup.sh
5354
config/vagrant/provision_vagrant.sh
@@ -310,6 +311,7 @@ intranet/apps/eighth/forms/admin/sponsors.py
310311
intranet/apps/eighth/management/__init__.py
311312
intranet/apps/eighth/management/commands/__init__.py
312313
intranet/apps/eighth/management/commands/absence_email.py
314+
intranet/apps/eighth/management/commands/absence_notify.py
313315
intranet/apps/eighth/management/commands/delete_duplicate_signups.py
314316
intranet/apps/eighth/management/commands/dev_create_blocks.py
315317
intranet/apps/eighth/management/commands/dev_generate_signups.py
@@ -387,6 +389,7 @@ intranet/apps/eighth/migrations/0062_auto_20200116_1926.py
387389
intranet/apps/eighth/migrations/0063_auto_20201224_1745.py
388390
intranet/apps/eighth/migrations/0064_auto_20210205_1153.py
389391
intranet/apps/eighth/migrations/0065_auto_20220903_0038.py
392+
intranet/apps/eighth/migrations/0066_auto_20240725_1929.py
390393
intranet/apps/eighth/migrations/__init__.py
391394
intranet/apps/eighth/tests/__init__.py
392395
intranet/apps/eighth/tests/eighth_test.py
@@ -602,10 +605,15 @@ intranet/apps/nomination/migrations/0001_initial.py
602605
intranet/apps/nomination/migrations/0002_auto_20160929_2156.py
603606
intranet/apps/nomination/migrations/__init__.py
604607
intranet/apps/notifications/__init__.py
608+
intranet/apps/notifications/api.py
605609
intranet/apps/notifications/emails.py
610+
intranet/apps/notifications/forms.py
606611
intranet/apps/notifications/models.py
612+
intranet/apps/notifications/serializers.py
607613
intranet/apps/notifications/tasks.py
614+
intranet/apps/notifications/tests.py
608615
intranet/apps/notifications/urls.py
616+
intranet/apps/notifications/utils.py
609617
intranet/apps/notifications/views.py
610618
intranet/apps/notifications/migrations/0001_initial.py
611619
intranet/apps/notifications/migrations/0002_auto_20150729_1734.py
@@ -614,6 +622,11 @@ intranet/apps/notifications/migrations/0004_notificationconfig_android_gcm_optou
614622
intranet/apps/notifications/migrations/0005_auto_20151221_2008.py
615623
intranet/apps/notifications/migrations/0006_auto_20151221_2028.py
616624
intranet/apps/notifications/migrations/0007_auto_20151221_2259.py
625+
intranet/apps/notifications/migrations/0008_userpushnotificationpreferences.py
626+
intranet/apps/notifications/migrations/0009_userpushnotificationpreferences_is_subscribed.py
627+
intranet/apps/notifications/migrations/0010_remove_userpushnotificationpreferences_silent_notifications.py
628+
intranet/apps/notifications/migrations/0011_webpushnotification.py
629+
intranet/apps/notifications/migrations/0012_auto_20240730_1928.py
617630
intranet/apps/notifications/migrations/__init__.py
618631
intranet/apps/oauth/__init__.py
619632
intranet/apps/oauth/admin.py
@@ -647,6 +660,7 @@ intranet/apps/polls/__init__.py
647660
intranet/apps/polls/admin.py
648661
intranet/apps/polls/forms.py
649662
intranet/apps/polls/models.py
663+
intranet/apps/polls/notifications.py
650664
intranet/apps/polls/tests.py
651665
intranet/apps/polls/urls.py
652666
intranet/apps/polls/views.py
@@ -882,7 +896,6 @@ intranet/settings/secret.sample
882896
intranet/static/browserconfig.xml
883897
intranet/static/manifest.json
884898
intranet/static/robots.txt
885-
intranet/static/serviceworker.js
886899
intranet/static/teacher-guide.pdf
887900
intranet/static/css/_colors.scss
888901
intranet/static/css/_reset.scss
@@ -992,6 +1005,7 @@ intranet/static/img/favicon/favicon.ico
9921005
intranet/static/img/favicon/favicon.svg
9931006
intranet/static/img/favicon/mstile-150x150.png
9941007
intranet/static/img/favicon/safari-pinned-tab.svg
1008+
intranet/static/img/guides/add_to_home_screen_ios.png
9951009
intranet/static/img/logos/Header-Logo.png
9961010
intranet/static/img/logos/Header-Logo.svg
9971011
intranet/static/img/logos/[email protected]
@@ -3472,6 +3486,11 @@ intranet/templates/lostfound/lostitem_form.html
34723486
intranet/templates/monitoring/prometheus-metrics.txt
34733487
intranet/templates/notifications/gcm_list.html
34743488
intranet/templates/notifications/gcm_post.html
3489+
intranet/templates/notifications/ios_notifications_guide.html
3490+
intranet/templates/notifications/webpush_device_info.html
3491+
intranet/templates/notifications/webpush_list.html
3492+
intranet/templates/notifications/webpush_post.html
3493+
intranet/templates/notifications/js/serviceworker.js
34753494
intranet/templates/oauth2_provider/application_confirm_delete.html
34763495
intranet/templates/oauth2_provider/application_detail.html
34773496
intranet/templates/oauth2_provider/application_form.html

Ion.egg-info/requires.txt

+4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ sphinx-bootstrap-theme==0.8.1
5959
tblib==1.7.0
6060
vine==5.0.0
6161
xhtml2pdf==0.2.11
62+
django-push-notifications[WP]==3.1.0
63+
py-vapid==1.9.1
64+
pywebpush==2.0.0
6265
asgiref>=3.3.4
6366
pillow>=9.0.0
6467
tinycss2
6568
twisted>=21.7.0
69+
python-bidi==0.4.2

config/docker/initial_setup.sh

+3
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ python3 -u manage.py import_sports $(date +%m)
4949

5050
echo -e "${BLUE}${BOLD}Creating CSL apps...${CLEAR}"
5151
python3 -u manage.py dev_create_cslapps
52+
53+
echo -e "${BLUE}${BOLD}Generating vapid keys...${CLEAR}"
54+
python3 create_vapid_keys.py

config/scripts/create_vapid_keys.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import base64
2+
import os
3+
4+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
5+
from py_vapid import Vapid
6+
7+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8+
9+
# Generate VAPID key pair
10+
vapid = Vapid()
11+
vapid.generate_keys()
12+
13+
# Get public and private keys for the vapid key pair
14+
vapid.save_public_key(os.path.join(PROJECT_ROOT, "keys", "webpush", "public_key.pem"))
15+
public_key_bytes = vapid.public_key.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
16+
17+
vapid.save_key(os.path.join(PROJECT_ROOT, "keys", "webpush", "private_key.pem"))
18+
19+
20+
# Convert the public key to applicationServerKey format
21+
application_server_key = base64.urlsafe_b64encode(public_key_bytes).replace(b"=", b"").decode("utf8")
22+
23+
with open(os.path.join(PROJECT_ROOT, "keys", "webpush", "ApplicationServerKey.key"), "w", encoding="utf-8") as f:
24+
f.write(application_server_key)

cron/eighth-absence.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
timestamp=$(date +"%Y-%m-%d-%H%M")
55
cd /usr/local/www/intranet3
66
./cron/env.sh ./manage.py absence_email --silent
7-
echo "Absence email sent at $timestamp." >> /var/log/ion/email.log
7+
./cron/env.sh ./manage.py absence_notify --silent
8+
echo "Absence email and push notification sent at $timestamp." >> /var/log/ion/email.log

docs/sourcedoc/intranet.apps.eighth.management.commands.rst

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ intranet.apps.eighth.management.commands.absence\_email module
1212
:undoc-members:
1313
:show-inheritance:
1414

15+
intranet.apps.eighth.management.commands.absence\_notify module
16+
---------------------------------------------------------------
17+
18+
.. automodule:: intranet.apps.eighth.management.commands.absence_notify
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
22+
1523
intranet.apps.eighth.management.commands.delete\_duplicate\_signups module
1624
--------------------------------------------------------------------------
1725

docs/sourcedoc/intranet.apps.notifications.rst

+40
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ intranet.apps.notifications package
44
Submodules
55
----------
66

7+
intranet.apps.notifications.api module
8+
--------------------------------------
9+
10+
.. automodule:: intranet.apps.notifications.api
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
715
intranet.apps.notifications.emails module
816
-----------------------------------------
917

@@ -12,6 +20,14 @@ intranet.apps.notifications.emails module
1220
:undoc-members:
1321
:show-inheritance:
1422

23+
intranet.apps.notifications.forms module
24+
----------------------------------------
25+
26+
.. automodule:: intranet.apps.notifications.forms
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
1531
intranet.apps.notifications.models module
1632
-----------------------------------------
1733

@@ -20,6 +36,14 @@ intranet.apps.notifications.models module
2036
:undoc-members:
2137
:show-inheritance:
2238

39+
intranet.apps.notifications.serializers module
40+
----------------------------------------------
41+
42+
.. automodule:: intranet.apps.notifications.serializers
43+
:members:
44+
:undoc-members:
45+
:show-inheritance:
46+
2347
intranet.apps.notifications.tasks module
2448
----------------------------------------
2549

@@ -28,6 +52,14 @@ intranet.apps.notifications.tasks module
2852
:undoc-members:
2953
:show-inheritance:
3054

55+
intranet.apps.notifications.tests module
56+
----------------------------------------
57+
58+
.. automodule:: intranet.apps.notifications.tests
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:
62+
3163
intranet.apps.notifications.urls module
3264
---------------------------------------
3365

@@ -36,6 +68,14 @@ intranet.apps.notifications.urls module
3668
:undoc-members:
3769
:show-inheritance:
3870

71+
intranet.apps.notifications.utils module
72+
----------------------------------------
73+
74+
.. automodule:: intranet.apps.notifications.utils
75+
:members:
76+
:undoc-members:
77+
:show-inheritance:
78+
3979
intranet.apps.notifications.views module
4080
----------------------------------------
4181

docs/sourcedoc/intranet.apps.polls.rst

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ intranet.apps.polls.models module
2828
:undoc-members:
2929
:show-inheritance:
3030

31+
intranet.apps.polls.notifications module
32+
----------------------------------------
33+
34+
.. automodule:: intranet.apps.polls.notifications
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:
38+
3139
intranet.apps.polls.tests module
3240
--------------------------------
3341

intranet/apps/announcements/forms.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ def __init__(self, *args, **kwargs):
1212
super().__init__(*args, **kwargs)
1313
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
1414

15-
self.fields["notify_post"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
15+
self.fields["notify_post"].help_text = (
16+
"If this box is checked, students who have signed up for email "
17+
"notifications will receive an email "
18+
"and those who have signed up for push notifications will receive a "
19+
"push notification."
20+
)
1621

1722
self.fields["notify_email_all"].help_text = (
1823
"This will send an email notification to all of the users who can see this post. This option "
@@ -41,7 +46,12 @@ def __init__(self, *args, **kwargs):
4146
super().__init__(*args, **kwargs)
4247
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
4348

44-
self.fields["notify_post_resend"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
49+
self.fields["notify_post_resend"].help_text = (
50+
"If this box is checked, students who have signed up for email "
51+
"notifications will receive an email "
52+
"and those who have signed up for push notifications will "
53+
"receive a push notification."
54+
)
4555

4656
self.fields["notify_email_all_resend"].help_text = (
4757
"This will resend an email notification to all of the users who can see this post. This option "
@@ -105,7 +115,12 @@ class AnnouncementAdminForm(forms.Form):
105115

106116
def __init__(self, *args, **kwargs):
107117
super().__init__(*args, **kwargs)
108-
self.fields["notify_post"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
118+
self.fields["notify_post"].help_text = (
119+
"If this box is checked, students who have signed up for email "
120+
"notifications will receive an email "
121+
"and those who have signed up for push notifications will receive a "
122+
"push notification."
123+
)
109124
self.fields["notify_email_all"].help_text = (
110125
"This will send an email notification to all of the users who can see this post. This option "
111126
"does NOT take users' email notification preferences into account, so please use with care."

intranet/apps/announcements/notifications.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@
33
import re
44

55
import requests
6+
from push_notifications.models import WebPushDevice
67
from requests_oauthlib import OAuth1
78
from sentry_sdk import capture_exception
89

910
from django.conf import settings
1011
from django.contrib import messages
1112
from django.contrib.auth import get_user_model
1213
from django.core import exceptions
14+
from django.db.models import Q
1315
from django.urls import reverse
16+
from django.utils.html import strip_tags
1417

1518
from ...utils.date import get_senior_graduation_year
16-
from ..notifications.tasks import email_send_task
19+
from ..notifications.tasks import email_send_task, send_bulk_notification
20+
from ..notifications.utils import truncate_content, truncate_title
21+
from ..users.models import User
22+
from .models import Announcement
1723

1824
logger = logging.getLogger(__name__)
1925

@@ -136,7 +142,7 @@ def announcement_posted_email(request, obj, send_all=False):
136142
emails.append(u.notification_email)
137143
users_send.append(u)
138144

139-
if not settings.PRODUCTION and len(emails) > 3:
145+
if not settings.PRODUCTION and len(emails) > 3 and not settings.FORCE_EMAIL_SEND:
140146
raise exceptions.PermissionDenied("You're about to email a lot of people, and you aren't in production!")
141147

142148
base_url = request.build_absolute_uri(reverse("index"))
@@ -201,3 +207,27 @@ def notify_twitter(status):
201207
req = requests.post(url, data=data, auth=auth, timeout=15)
202208

203209
return req.text
210+
211+
212+
def announcement_posted_push_notification(obj: Announcement) -> None:
213+
"""Send a (Web)push notification to users when an announcement is posted.
214+
215+
obj: The announcement object
216+
217+
"""
218+
219+
if not obj.groups.all():
220+
users = User.objects.filter(push_notification_preferences__announcement_notifications=True)
221+
devices = WebPushDevice.objects.filter(user__in=users)
222+
else:
223+
users = User.objects.filter(Q(groups__in=obj.groups.all()) & Q(push_notification_preferences__announcement_notifications=True))
224+
devices = WebPushDevice.objects.filter(user__in=users)
225+
226+
send_bulk_notification.delay(
227+
filtered_objects=devices,
228+
title=f"Announcement: {truncate_title(obj.title)} ({obj.get_author()})",
229+
body=truncate_content(strip_tags(obj.content_no_links)),
230+
data={
231+
"url": settings.PUSH_NOTIFICATIONS_BASE_URL + reverse("view_announcement", args=[obj.id]),
232+
},
233+
)

intranet/apps/announcements/views.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from ..groups.models import Group
1616
from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm
1717
from .models import Announcement, AnnouncementRequest
18-
from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
19-
request_announcement_email)
18+
from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email,
19+
announcement_posted_push_notification, announcement_posted_twitter, request_announcement_email)
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -43,6 +43,7 @@ def announcement_posted_hook(request, obj):
4343
"""
4444
if obj.notify_post:
4545
announcement_posted_twitter(request, obj)
46+
announcement_posted_push_notification(obj)
4647
try:
4748
notify_all = obj.notify_email_all
4849
except AttributeError:

0 commit comments

Comments
 (0)