Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Add admin user (optional)

The emails notifications that Kukkuu sends can be imported from a Google Sheets spreadsheet. To do that, first configure setting `KUKKUU_NOTIFICATIONS_SHEET_ID`, and then either

1. run `python manage.py import_notifications` to import and update all the notifications, or
1. run `python manage.py import_notifications_from_google_sheets` to import and update all the notifications, or
2. use actions in Django admin UI's notification list view to have finer control on which notifications to update and create

The notification templates primary import source is https://docs.google.com/spreadsheets/d/1TkdQsO50DHOg5pi1JhzudOL1GKpiK-V2DCIoAipKj-M.
Expand All @@ -168,6 +168,10 @@ The environment variable should then be set to:
KUKKUU_NOTIFICATIONS_SHEET_ID=1TkdQsO50DHOg5pi1JhzudOL1GKpiK-V2DCIoAipKj-M
```

this is then set to `NOTIFICATIONS_SHEET_ID` in `settings.py`.

See more detailed instructions and other importer options from [notification_importers/README.md](./notification_importers/README.md).

#### Daily running, Debugging

- Create `.env` file: `touch .env`
Expand All @@ -187,6 +191,7 @@ For local development, if you prefer, you can alternatively use a shorter, manua
Here's how you can generate a value for `SECRET_KEY` using Python (Based on Django v5.1.6's
[get_random_secret_key](https://github.com/django/django/blob/5.1.6/django/core/management/utils.py#L79C5-L84) &
[get_random_string](https://github.com/django/django/blob/5.1.6/django/utils/crypto.py#L51-L62)):

```python
import secrets, string
allowed_chars = string.ascii_lowercase + string.digits + "!@#$%^&*(-_=+)"
Expand Down Expand Up @@ -243,16 +248,15 @@ This project uses [Ruff](https://docs.astral.sh/ruff/) for code formatting and q

Basic `ruff` commands:

* lint: `ruff check`
* apply safe lint fixes: `ruff check --fix`
* check formatting: `ruff format --check`
* format: `ruff format`
- lint: `ruff check`
- apply safe lint fixes: `ruff check --fix`
- check formatting: `ruff format --check`
- format: `ruff format`

[`pre-commit`](https://pre-commit.com/) can be used to install and
run all the formatting tools as git hooks automatically before a
commit.


### Commit message format

New commit messages must adhere to the [Conventional Commits](https://www.conventionalcommits.org/)
Expand All @@ -261,7 +265,6 @@ specification, and line length is limited to 72 characters.
When [`pre-commit`](https://pre-commit.com/) is in use, [`commitlint`](https://github.com/conventional-changelog/commitlint)
checks new commit messages for the correct format.


## Application programming interfaces

### GraphQL API Documentation
Expand Down
9 changes: 0 additions & 9 deletions importers/templates/notification_change_list.html

This file was deleted.

21 changes: 0 additions & 21 deletions importers/tests/snapshots/snap_test_notification_importer.py

This file was deleted.

11 changes: 6 additions & 5 deletions kukkuu/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def sentry_before_send(event: Event, hint: Hint):
"languages",
"subscriptions",
"messaging",
"importers",
"notification_importers",
"reports",
"verification_tokens",
"auditlog_extra",
Expand Down Expand Up @@ -339,11 +339,9 @@ def sentry_before_send(event: Event, hint: Hint):
"fonts.googleapis.com",
]

# CSP_SCRIPT_SRC includes 'unsafe-eval' for inline scripts added by
# `SpectacularRedocView`

CSP_SCRIPT_SRC = [
CSP.SELF,
CSP.UNSAFE_EVAL,
"cdn.jsdelivr.net",
"blob:",
]
Expand Down Expand Up @@ -471,11 +469,14 @@ def sentry_before_send(event: Event, hint: Hint):
KUKKUU_DEFAULT_ENROLMENT_LIMIT = 2
KUKKUU_REMINDER_DAYS_IN_ADVANCE = env("KUKKUU_REMINDER_DAYS_IN_ADVANCE")
KUKKUU_FEEDBACK_NOTIFICATION_DELAY = env("KUKKUU_FEEDBACK_NOTIFICATION_DELAY")
KUKKUU_NOTIFICATIONS_SHEET_ID = env("KUKKUU_NOTIFICATIONS_SHEET_ID")
# How many hours before the occurrence start time
# the user can unenrol from the occurrence.
# If set to 0, unenrolment is allowed till the occurrence start.
KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE = env("KUKKUU_ENROLMENT_UNENROL_HOURS_BEFORE")
NOTIFICATIONS_IMPORTER = (
"notification_importers.notification_importer.NotificationGoogleSheetImporter"
)
NOTIFICATIONS_SHEET_ID = env("KUKKUU_NOTIFICATIONS_SHEET_ID")

KUKKUU_HASHID_MIN_LENGTH = 5
KUKKUU_HASHID_ALPHABET = "abcdefghijklmnopqrstuvwxyz"
Expand Down
7 changes: 7 additions & 0 deletions kukkuu/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ def graphql_view(request, *args, **kwargs):
*URL_PATTERNS_FOR_DJANGO_ADMIN_KEYCLOAK_LOGIN,
path("admin/", admin.site.urls),
re_path(r"^graphql/?$", graphql_view),
path(
"notification_importers/",
include(
("notification_importers.urls", "notification_importers"),
namespace="notification_importers",
),
),
path("reports/", include(router.urls)),
path("reports/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
Expand Down
38 changes: 38 additions & 0 deletions notification_importers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [Notification Importer](#notification-importer)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Notification Importer

The notification templates can be imported via

- a) Google sheet importer
- b) Template file importer

The importer can be used to create and update the notification templates or to check whether they are in sync.
The importer can be used via Django management commands (in notification_importers app) or admin site tools.

To enable admin site tools, some configuration is needed:

To enable a selected importer (`NotificationFileImporter` or `NotificationGoogleSheetImporter`)

```python
NOTIFICATIONS_IMPORTER = (
"notification_importers.notification_importer.NotificationFileImporter"
)
```

If a Google sheet importer is used, also `NOTIFICATIONS_SHEET_ID` is needed

```python
NOTIFICATIONS_SHEET_ID = "1234"
```

If a File importer is used, files should be stored in notification_importers app in
notification_importers/templates/sms and notification_importers/templates/email folders.
There is also a naming convention used there. The file name must be given in this pattern
[notification_type]-[locale].[html|j2].
File renamed without changes.
52 changes: 43 additions & 9 deletions importers/admin.py → notification_importers/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import importlib
import logging

from csp.decorators import csp_update
from django.conf import settings
from django.contrib import admin, messages
from django.http import HttpResponseRedirect
Expand All @@ -9,7 +11,12 @@
from django_ilmoitin.admin import NotificationTemplateAdmin
from django_ilmoitin.models import NotificationTemplate

from .notification_importer import NotificationImporter, NotificationImporterException
from kukkuu.consts import CSP

from .notification_importer import (
AbstractNotificationImporter,
NotificationImporterError,
)

logger = logging.getLogger(__name__)

Expand All @@ -19,19 +26,28 @@ class NotificationTemplateAdminWithImporter(NotificationTemplateAdmin):
actions = ("update_selected",)
ordering = ("type",)
change_list_template = "notification_change_list.html"
importer = None
importer: AbstractNotificationImporter = None

def changelist_view(self, request, *args, **kwargs):
try:
self.importer = NotificationImporter()
except NotificationImporterException as e:
self.importer = self.__load_importer_class_from_settings()
except NotificationImporterError as e:
self._send_error_message(request, e)
return super().changelist_view(request, *args, **kwargs)

def __load_importer_class_from_settings(self) -> AbstractNotificationImporter:
(
importer_module_name,
importer_class_name,
) = settings.NOTIFICATIONS_IMPORTER.rsplit(".", 1)
importer_module = importlib.import_module(importer_module_name)
importer_class = getattr(importer_module, importer_class_name)
return importer_class()

def get_sync_status(self, obj):
return self.importer.is_notification_in_sync(obj) if self.importer else None

get_sync_status.short_description = _("in sync with the spreadsheet")
get_sync_status.short_description = _("in sync with the importer source")
get_sync_status.boolean = True

def update_selected(self, request, queryset):
Expand All @@ -40,7 +56,7 @@ def update_selected(self, request, queryset):

try:
num_of_updated = self.importer.update_notifications(queryset)
except NotificationImporterException as e:
except NotificationImporterError as e:
self._send_error_message(request, e)
return

Expand All @@ -55,7 +71,7 @@ def update_selected(self, request, queryset):
self.message_user(request, message, messages.SUCCESS)

update_selected.short_description = _(
"Update selected notifications from the spreadsheet"
"Update selected notifications from the importer source"
)

def get_urls(self):
Expand All @@ -66,14 +82,22 @@ def get_urls(self):
self.admin_site.admin_view(self.import_missing_notifications),
name="import-missing-notifications",
),
path(
"export-notification-templates/",
self.admin_site.admin_view(self.export_notifications_csv),
name="export-notification-templates",
),
]
return custom_urls + urls

def export_notifications_csv(self, request):
return HttpResponseRedirect(reverse("export-notification-templates-csv"))

def import_missing_notifications(self, request):
if self.importer:
try:
num_of_created = self.importer.create_missing_notifications()
except NotificationImporterException as e:
except NotificationImporterError as e:
self._send_error_message(request, e)
else:
if num_of_created:
Expand All @@ -94,7 +118,17 @@ def _send_error_message(self, request, message):
logger.error(message)
self.message_user(request, message, messages.ERROR)

# Acecpt CSP_UNSAFE_INLINE for the changeform view to allow
# the preview template to work correctly.
@csp_update(
SCRIPT_SRC=settings.CSP_SCRIPT_SRC + [CSP.UNSAFE_INLINE],
)
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
return super().changeform_view(
request, object_id, form_url, extra_context=extra_context
)


if settings.KUKKUU_NOTIFICATIONS_SHEET_ID:
if hasattr(settings, "NOTIFICATIONS_IMPORTER"):
admin.site.unregister(NotificationTemplate)
admin.site.register(NotificationTemplate, NotificationTemplateAdminWithImporter)
Loading