Skip to content

Commit

Permalink
Merge pull request #656 from euphorie/scrum-1572--reminders
Browse files Browse the repository at this point in the history
Euphorie notifications
  • Loading branch information
thet authored Nov 17, 2023
2 parents 147eb13 + dd55b49 commit 5d1796f
Show file tree
Hide file tree
Showing 51 changed files with 1,778 additions and 3 deletions.
11 changes: 11 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ Changelog
15.1.0 (unreleased)
-------------------

- Notifications:
- Add translations.
- Add basic notification for risk assessments which were not modified since a configurable number of days.
Note: This notification is disabled by default.
- Add script to send notifications via cron.
- Add basic notifications infrastructure.
- Allow to set notification subscription settings in the preferences panel.
Note: the notification subscriptions are per default hidden.
- Add BaseEmail class to unify sending emails.
Ref: scrum-1572
[thet, reinhardt]
- Add behavior to hide modules from trainings.
Ref: scrum-1573
[thet]
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"setuptools",
"scikit-learn",
"sh",
"stoneagehtml",
"user_agents",
"z3c.form",
"z3c.schema",
Expand Down
70 changes: 68 additions & 2 deletions src/euphorie/client/browser/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"""
from Acquisition import aq_inner
from euphorie.client import MessageFactory as _
from euphorie.client.interfaces import INotificationCategory
from euphorie.client.model import Account
from euphorie.client.model import AccountChangeRequest
from euphorie.client.model import get_current_account
from euphorie.client.model import NotificationSubscription
from euphorie.client.utils import CreateEmailTo
from euphorie.client.utils import randomString
from plone import api
Expand All @@ -29,6 +31,7 @@
from z3c.saconfig import Session
from z3c.schema.email import RFC822MailAddress
from zope import schema
from zope.component import getAdapters
from zope.i18n import translate
from zope.interface import alsoProvides
from zope.interface import directlyProvides
Expand Down Expand Up @@ -136,11 +139,67 @@ class Preferences(AutoExtensibleForm, form.Form):

schema = PreferencesSchema

show_personal_details = True
show_notifications = False

@property
@memoize
def current_user(self):
return get_current_account()

def getContent(self):
user = get_current_account()
user = self.current_user
directlyProvides(user, PreferencesSchema)
return user

@property
def all_notifications(self):
notifications = getAdapters((self.context, self.request), INotificationCategory)
# Return the available notifications, sorted by title.
notifications = [
notification[1]
for notification in notifications
if notification[1].available
]
return sorted(
notifications,
key=lambda notification: translate(notification.title),
)

@property
@memoize
def existing_notification_subscriptions(self):
subscriptions = Session.query(NotificationSubscription).filter(
NotificationSubscription.account_id == self.current_user.getId()
)
return {subscription.category: subscription for subscription in subscriptions}

def subscribe_notification(self, category_id):
notification = self.existing_notification_subscriptions.get(category_id)
if notification:
notification.enabled = True
return
Session.add(
NotificationSubscription(
account_id=self.current_user.getId(),
category=category_id,
enabled=True,
)
)

def unsubscribe_notification(self, category_id):
notification = self.existing_notification_subscriptions.get(category_id)
if notification:
notification.enabled = False
return
Session.add(
NotificationSubscription(
account_id=self.current_user.getId(),
category=category_id,
enabled=False,
)
)

@button.buttonAndHandler(_("Save"), name="save")
def handleSave(self, action):
flash = IStatusMessage(self.request).addStatusMessage
Expand All @@ -149,10 +208,17 @@ def handleSave(self, action):
for error in errors:
flash(error.message, "notice")
return
user = get_current_account()
user = self.current_user
user.first_name = data["first_name"]
user.last_name = data["last_name"]

if self.show_notifications:
for notification in self.all_notifications:
if self.request.get("notifications", {}).get(notification.id):
self.subscribe_notification(notification.id)
else:
self.unsubscribe_notification(notification.id)


class AccountSettings(AutoExtensibleForm, form.Form):
"""View name: @@account-settings."""
Expand Down
33 changes: 33 additions & 0 deletions src/euphorie/client/browser/templates/preferences.pt
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
<div class="container"
id="app-content-preferences"
>

<fieldset class="form-panel pat-collapsible true section horizontal has-value open focus"
id="patterns-label-personal-details"
data-pat-collapsible="store: local; scroll-selector: self; scroll-offset: 100px"
tal:condition="view/show_personal_details"
>
<h3 class="form-separation-header collapsible-open"
i18n:translate="title_personal_details"
Expand All @@ -57,6 +59,37 @@
</tal:widget>
</div>
</fieldset>

<fieldset class="form-panel pat-collapsible true section horizontal"
id="label-notifications"
data-pat-collapsible="store: local; scroll-selector: self; scroll-offset: 100px"
tal:define="
notifications view/all_notifications;
"
tal:condition="python: view.show_notifications and notifications"
>

<h3 class="form-separation-header"
i18n:translate="label_mailings"
>Notifications
</h3>
<fieldset class="pat-checklist group checkbox">
<tal:loop repeat="notification notifications">
<label class="">
<input checked="${python:'checked' if getattr(view.existing_notification_subscriptions.get(notification.id, None), 'enabled', notification.default) else None}"
name="notifications.${notification/id}:boolean:record"
type="checkbox"
value="true"
/>
<strong class="label-fragment-title">${notification/title}</strong>
<p class="label-fragment-description">${notification/description}</p>
</label>
<br />
</tal:loop>
</fieldset>

</fieldset>

</div>
</form>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/euphorie/client/browser/webhelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,5 +1426,11 @@ def survey_tree_data(self):

return data

def get_user_email(self, account=None):
return account.email

def get_user_fullname(self, account=None):
return account.title

def __call__(self):
return self
2 changes: 2 additions & 0 deletions src/euphorie/client/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

<include package=".adapters" />
<include package=".browser" />
<include package=".notifications" />
<include package=".mails" />
<include package=".subscribers" />
<include package=".docx" />
<include package=".widgets" />
Expand Down
25 changes: 25 additions & 0 deletions src/euphorie/client/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from plonetheme.nuplone.z3cform.interfaces import INuPloneFormLayer
from zope.interface import Attribute
from zope.interface import implementer
from zope.interface import Interface
from zope.interface.interfaces import ObjectEvent
from zope.publisher.interfaces.browser import IDefaultBrowserLayer


INTERVAL_DAILY = "daily"
INTERVAL_HOURLY = "hourly"
INTERVAL_MINUTELY = "minutely"
INTERVAL_MANUAL = "manual"


class IClientSkinLayer(IDefaultBrowserLayer, INuPloneFormLayer):
"""Zope skin layer for the online client."""

Expand All @@ -16,3 +23,21 @@ class ICustomRisksModifiedEvent(Interface):
@implementer(ICustomRisksModifiedEvent)
class CustomRisksModifiedEvent(ObjectEvent):
"""Custom risks were modified."""


class INotificationCategory(Interface):
"""A notification category adapter."""

id = Attribute("Id") # The id of the category
title = Attribute("Title") # The title of the category
description = Attribute("Description") # A description of the category

# The interval, this notification should be checked.
# One of "daily", "hourly", "minutely"
interval = Attribute("Interval")

# Return True if this category is available for the current user.
available = Attribute("Available")

def notify():
"""Send a notification."""
Empty file.
Loading

0 comments on commit 5d1796f

Please sign in to comment.