-
Notifications
You must be signed in to change notification settings - Fork 1
feat(notifications): implement transactional email notification system #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thejoeejoee
wants to merge
34
commits into
develop
Choose a base branch
from
feat-notifications
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
20feabf
feat(notifications): implement transactional email notification system
thejoeejoee 0981c6f
Merge branch 'develop' into feat-notifications
thejoeejoee 82ee3ef
feat(notifications): impl
thejoeejoee 3a2019e
fix: dockerfile
thejoeejoee 1779077
feat(notifications): integrate email notifications into matching and …
thejoeejoee 197103d
fix(notifications): remove dead code, fix P0 bugs, deduplicate INSTAL…
thejoeejoee e94ae55
chore(notifications): delete orphan Path A template files
thejoeejoee 6379ff8
feat(notifications): add global email opt-out with preferences UI toggle
thejoeejoee ca0f9e5
feat(notifications): wire recipient_profile to all send_notification_…
thejoeejoee 0b63d5c
fix(CR): user/user_profile and other fixes
thejoeejoee 7c11169
fix(notifications): use CheckEnabledPluginsViewMixin for plugin detec…
thejoeejoee f0f44ed
chore(docker): drop deprecated compose version key
thejoeejoee b5be008
fix(sections): fall back to ROOT_DOMAIN when site domain mismatches
thejoeejoee d70a498
chore(dev): parametrize OrbStack domains with ROOT_DOMAIN
thejoeejoee 3a8c8e8
fix(notifications): use in_space_of_section instead of in_space_of
thejoeejoee c3d968a
fix(notifications): address code review findings from branch review
thejoeejoee 4b3f13a
fix(notifications): narrow bare except to ObjectDoesNotExist in mailer
thejoeejoee b1102a3
ci: add test workflow for pull requests
thejoeejoee b6d3982
fix(ci): add missing RECAPTCHA env vars for test workflow
thejoeejoee 7e78249
fix(ci): add BUILD_DIR, STATIC_ROOT, MEDIA_ROOT env vars for test wor…
thejoeejoee c217e36
fix(ci): install setuptools for pkg_resources compatibility
thejoeejoee 7e13401
fix(ci): use uv run --with setuptools for pkg_resources
thejoeejoee dd9e409
fix(ci): use venv python directly to preserve setuptools
thejoeejoee 38e276a
fix(ci): install setuptools directly into venv python
thejoeejoee a2a201a
fix(ci): use uv pip instead of python -m pip for setuptools
thejoeejoee 77edf6e
fix(ci): target venv explicitly for setuptools install
thejoeejoee ed3910f
fix(ci): pin setuptools<82 to retain pkg_resources
thejoeejoee 24dd583
fix(ci): make database host configurable via env var for CI
thejoeejoee bfdba9c
fix(ci): fix SMTP backend, import path, and soft-cancel sent_at rollback
thejoeejoee f8d4959
fix(notifications): regenerate migration, fix template .user access a…
thejoeejoee 3138c89
fix(tests): fix mock sections, stale factory field, and import path
thejoeejoee 4734755
fix(tests): check global email opt-out in match notifications and fix…
thejoeejoee 9bf7a3f
fix(tests): use empty whatsapp in UserProfileFactory to avoid validat…
thejoeejoee 3fbd6a1
fix(CR): address bot review findings (send return, PII, URLs, templat…
thejoeejoee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: Tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [develop, main] | ||
|
|
||
| concurrency: | ||
| group: tests-${{ github.head_ref || github.run_id }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:15 | ||
| env: | ||
| POSTGRES_DB: fiesta | ||
| POSTGRES_USER: fiesta | ||
| POSTGRES_PASSWORD: fiesta | ||
| ports: | ||
| - 5432:5432 | ||
| options: >- | ||
| --health-cmd="pg_isready -U fiesta" | ||
| --health-interval=10s | ||
| --health-timeout=5s | ||
| --health-retries=5 | ||
|
|
||
| env: | ||
| DATABASE_URL: postgres://fiesta:fiesta@localhost:5432/fiesta | ||
| DJANGO_SETTINGS_MODULE: fiesta.settings | ||
| DJANGO_CONFIGURATION: Development | ||
| DJANGO_SECRET_KEY: ci-test-secret-key-not-for-production | ||
| DJANGO_RECAPTCHA_SITE_KEY: dummy-recaptcha-site-key | ||
| DJANGO_RECAPTCHA_SECRET_KEY: dummy-recaptcha-secret-key | ||
| ROOT_DOMAIN: fiesta.test | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: astral-sh/setup-uv@v5 | ||
| with: | ||
| enable-cache: true | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync | ||
|
|
||
| - name: Run tests | ||
| working-directory: fiesta | ||
| run: uv run python manage.py test --verbosity 1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| version: '3.3' | ||
| services: | ||
| web: | ||
| build: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| version: '3.3' | ||
| name: fiesta-plus | ||
| services: | ||
| web: | ||
| command: python manage.py runserver 0.0.0.0:8000 | ||
|
|
||
18 changes: 18 additions & 0 deletions
18
fiesta/apps/accounts/migrations/0028_add_email_notifications_enabled.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Generated by Django 4.2.28 on 2026-03-06 23:52 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('accounts', '0027_userprofile_picture_height_userprofile_picture_width_and_more'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='userprofile', | ||
| name='email_notifications_enabled', | ||
| field=models.BooleanField(default=True, verbose_name='email notifications'), | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
fiesta/apps/buddy_system/migrations/0032_add_email_notification_fields.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Generated by Django 4.2.28 on 2026-03-03 14:17 | ||
|
|
||
| import datetime | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('buddy_system', '0031_alter_buddysystemconfiguration_rolling_limit_and_more'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='buddysystemconfiguration', | ||
| name='email_notify_issuer_delay', | ||
| field=models.DurationField(default=datetime.timedelta(seconds=3600), help_text='Gives editors time to correct the match before the student is notified.', verbose_name='Delay before notifying the issuer'), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='buddysystemconfiguration', | ||
| name='email_notify_on_match', | ||
| field=models.BooleanField(default=True, verbose_name='Send email notifications on match'), | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from django.contrib import admin | ||
| from django.utils import timezone | ||
| from django.utils.translation import gettext_lazy as _ | ||
|
|
||
| from apps.notifications.models import ScheduledNotification, SectionNotificationPreferences | ||
|
|
||
| if TYPE_CHECKING: | ||
| from django.db.models import QuerySet | ||
| from django.http import HttpRequest | ||
|
|
||
|
|
||
| @admin.register(SectionNotificationPreferences) | ||
| class SectionNotificationPreferencesAdmin(admin.ModelAdmin): | ||
| list_display = ["user", "section", "notify_on_match", "notify_on_new_member_waiting", "created"] | ||
| list_filter = ["section", "notify_on_match", "notify_on_new_member_waiting"] | ||
| # user is FK → accounts.User | ||
| search_fields = ["user__email", "user__first_name", "user__last_name", "section__name"] | ||
| readonly_fields = ["created", "modified"] | ||
|
|
||
|
|
||
| @admin.register(ScheduledNotification) | ||
| class ScheduledNotificationAdmin(admin.ModelAdmin): | ||
| list_display = ["kind", "recipient", "section", "send_after", "sent_at", "cancelled_at", "created"] | ||
| list_filter = ["kind", "section"] | ||
| # recipient is FK to accounts.User | ||
| search_fields = ["recipient__email", "recipient__first_name", "recipient__last_name"] | ||
| readonly_fields = ["created", "modified", "content_type", "object_id", "content_object"] | ||
| date_hierarchy = "send_after" | ||
| actions = ["mark_as_sent", "cancel_notifications"] | ||
|
|
||
| def get_queryset(self, request: HttpRequest) -> QuerySet[ScheduledNotification]: | ||
| return super().get_queryset(request).select_related("recipient", "section", "content_type") | ||
|
|
||
| @admin.action(description=_("Send now (bypass send_after)")) | ||
| def mark_as_sent(self, request: HttpRequest, queryset: QuerySet[ScheduledNotification]) -> None: | ||
| updated = queryset.filter(sent_at__isnull=True, cancelled_at__isnull=True).update(send_after=timezone.now()) | ||
| self.message_user(request, _("Scheduled %s notification(s) to send immediately.") % updated) | ||
|
|
||
| @admin.action(description=_("Cancel selected notifications")) | ||
| def cancel_notifications(self, request: HttpRequest, queryset: QuerySet[ScheduledNotification]) -> None: | ||
| count = 0 | ||
| for notification in queryset.filter(sent_at__isnull=True, cancelled_at__isnull=True): | ||
| notification.cancel() | ||
| notification.save(update_fields=["cancelled_at", "modified"]) | ||
| count += 1 | ||
| self.message_user(request, _("Cancelled %s notification(s).") % count) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.