Skip to content

Commit

Permalink
Sends email notifications for directory entries with publication date…
Browse files Browse the repository at this point in the history
… in future
  • Loading branch information
Tschuppi81 committed Jan 3, 2025
1 parent eb0b55a commit ce6222d
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 51 deletions.
8 changes: 7 additions & 1 deletion src/onegov/directory/models/directory_entry.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from datetime import datetime

from onegov.core.orm import Base
from onegov.core.orm.mixins import ContentMixin
from onegov.core.orm.mixins import TimestampMixin
from onegov.core.orm.mixins import UTCPublicationMixin
from onegov.core.orm.types import UUID
from onegov.core.orm.types import UUID, UTCDateTime
from onegov.file import AssociatedFiles
from onegov.gis import CoordinatesMixin
from onegov.search import SearchableContent
Expand Down Expand Up @@ -75,6 +77,10 @@ def es_public(self) -> bool:
#: Describes the entry briefly
lead: 'Column[str | None]' = Column(Text, nullable=True)

#: Marks the entry if publication notifications have been sent
notification_sent: 'Column[datetime | None]' = Column(UTCDateTime,
default=None)

#: All keywords defined for this entry (indexed)
_keywords: 'Column[dict[str, str] | None]' = Column( # type:ignore
MutableDict.as_mutable(HSTORE),
Expand Down
26 changes: 26 additions & 0 deletions src/onegov/directory/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,29 @@ def make_directory_models_polymorphic_type_non_nullable(
""")

context.operations.alter_column(table, 'type', nullable=False)

@upgrade_task('Directory entries add notification_sent column 1')

Check failure on line 71 in src/onegov/directory/upgrade.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (E302)

src/onegov/directory/upgrade.py:71:1: E302 Expected 2 blank lines, found 1
def add_notification_sent_column(context: UpgradeContext) -> None:
if not context.has_column('directory_entries', 'notification_sent'):
context.operations.add_column(
'directory_entries',
Column(
'notification_sent',
Boolean,

Check failure on line 78 in src/onegov/directory/upgrade.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F821)

src/onegov/directory/upgrade.py:78:17: F821 Undefined name `Boolean`
nullable=True,
default=False
)
)

# update existing entries to have notification_sent=False
context.operations.execute(f"""
UPDATE directory_entries SET notification_sent = FALSE
WHERE notification_sent IS NULL;
""")

Check failure on line 88 in src/onegov/directory/upgrade.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F541)

src/onegov/directory/upgrade.py:85:32: F541 f-string without any placeholders
context.session.flush()

# alter notification_sent non-nullable
context.operations.alter_column(
'directory_entries', 'notification_sent', nullable=False)

# TODO requires migration for past entries in cli command

Check failure on line 95 in src/onegov/directory/upgrade.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (W292)

src/onegov/directory/upgrade.py:95:62: W292 No newline at end of file
34 changes: 34 additions & 0 deletions src/onegov/org/cronjobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from onegov.core.orm import find_models
from onegov.core.orm.mixins.publication import UTCPublicationMixin
from onegov.core.templates import render_template
from onegov.directory import DirectoryCollection
from onegov.event import Occurrence, Event
from onegov.file import FileCollection
from onegov.form import FormSubmission, parse_form
Expand All @@ -17,10 +18,14 @@
from onegov.org.layout import DefaultMailLayout
from onegov.org.models import (
ResourceRecipient, ResourceRecipientCollection, TANAccess, News)
from onegov.org.models.directory import ExtendedDirectoryEntryCollection, \
ExtendedDirectory
from onegov.org.models.extensions import (
GeneralFileLinkExtension, DeletableContentExtension)
from onegov.org.models.ticket import ReservationHandler
from onegov.org.views.allocation import handle_rules_cronjob
from onegov.org.views.directory import \
send_email_notification_for_directory_entry
from onegov.org.views.newsletter import send_newsletter
from onegov.org.views.ticket import delete_tickets_and_related_data
from onegov.reservation import Reservation, Resource, ResourceCollection
Expand Down Expand Up @@ -69,6 +74,7 @@ def hourly_maintenance_tasks(request: 'OrgRequest') -> None:
publish_files(request)
reindex_published_models(request)
send_scheduled_newsletter(request)
send_email_notification_for_recent_directory_entry_publications(request)
delete_old_tans(request)
delete_old_tan_accesses(request)

Expand All @@ -84,6 +90,34 @@ def send_scheduled_newsletter(request: 'OrgRequest') -> None:
newsletter.scheduled = None


def send_email_notification_for_recent_directory_entry_publications(
request: 'OrgRequest'
) -> None:
"""
Sends notifications to users about recently published `DirectoryEntry`.
"""

now = utcnow()

for directory in DirectoryCollection(request.session).query().filter(
ExtendedDirectory.enable_update_notifications == True):

directory_entries = ExtendedDirectoryEntryCollection(
directory).query().filter(
and_(
now >= ExtendedDirectoryEntry.publication_start, # test for published?

Check failure on line 109 in src/onegov/org/cronjobs.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (E501)

src/onegov/org/cronjobs.py:109:80: E501 Line too long (91 > 79)
ExtendedDirectoryEntry.notification_sent == None,
)
)
for entry in directory_entries:
assert entry.published

send_email_notification_for_directory_entry(
directory, entry, request)
entry.notification_sent = now


def publish_files(request: 'OrgRequest') -> None:
FileCollection(request.session).publish_files()

Expand Down
101 changes: 52 additions & 49 deletions src/onegov/org/views/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,56 @@ def as_dict(entry: Any) -> dict[str, Any]:
return tuple(as_dict(e) for e in entries)


def send_email_notification_for_directory_entry(
directory,
entry,
request
) -> None:
title = request.translate(_(
'${org}: New Entry in "${directory}"',
mapping={'org': request.app.org.title,
'directory': directory.title},
))
entry_link = request.link(entry)
recipients = EntryRecipientCollection(request.session).query(
).filter_by(directory_id=directory.id).filter_by(
confirmed=True).all()

def email_iter() -> 'Iterator[EmailJsonDict]':
for recipient in recipients:
unsubscribe = request.link(
recipient.subscription, 'unsubscribe')
content = render_template(
'mail_new_directory_entry.pt',
request,
{
'layout': DefaultMailLayout(object(), request),
'title': title,
'directory': directory,
'entry_title': entry.title,
'entry_link': entry_link,
'unsubscribe': unsubscribe
},
)
plaintext = html_to_text(content)
print('*** tschupre sending notification to', recipient.address)
yield request.app.prepare_email(
receivers=(recipient.address,),
subject=title,
content=content,
plaintext=plaintext,
category='transactional',
attachments=(),
headers={
'List-Unsubscribe': f'<{unsubscribe}>',
'List-Unsubscribe-Post': (
'List-Unsubscribe=One-Click')
}
)

request.app.send_transactional_email_batch(email_iter())


@OrgApp.form(
model=ExtendedDirectoryEntryCollection,
permission=Private,
Expand Down Expand Up @@ -601,59 +651,12 @@ def handle_new_directory_entry(
transaction.abort()
return request.redirect(request.link(self))

# FIXME: if this entry is not yet published we will need to send
# a notification using some kind of cronjob, but we need
# to take care to only send it once, so we probably need
# to add a marker to entries to indicate that notifications
# have already been sent.
if self.directory.enable_update_notifications and entry.access in (
'public',
'mtan'
) and entry.published:
title = request.translate(_(
'${org}: New Entry in "${directory}"',
mapping={'org': request.app.org.title,
'directory': self.directory.title},
))

entry_link = request.link(entry)

recipients = EntryRecipientCollection(request.session).query(
).filter_by(directory_id=self.directory.id).filter_by(
confirmed=True).all()

def email_iter() -> 'Iterator[EmailJsonDict]':
for recipient in recipients:
unsubscribe = request.link(
recipient.subscription, 'unsubscribe')
content = render_template(
'mail_new_directory_entry.pt',
request,
{
'layout': DefaultMailLayout(object(), request),
'title': title,
'directory': self.directory,
'entry_title': entry.title,
'entry_link': entry_link,
'unsubscribe': unsubscribe
},
)
plaintext = html_to_text(content)
yield request.app.prepare_email(
receivers=(recipient.address,),
subject=title,
content=content,
plaintext=plaintext,
category='transactional',
attachments=(),
headers={
'List-Unsubscribe': f'<{unsubscribe}>',
'List-Unsubscribe-Post': (
'List-Unsubscribe=One-Click')
}
)

request.app.send_transactional_email_batch(email_iter())
send_email_notification_for_directory_entry(
self.directory, entry, request)

request.success(_('Added a new directory entry'))
return request.redirect(request.link(entry))
Expand Down
105 changes: 104 additions & 1 deletion tests/onegov/org/test_cronjobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from onegov.directory import (DirectoryEntryCollection,
DirectoryConfiguration,
DirectoryCollection)
from onegov.directory.collections.directory import EntryRecipientCollection
from onegov.event import EventCollection, OccurrenceCollection
from onegov.event.utils import as_rdates
from onegov.form import FormSubmissionCollection
Expand Down Expand Up @@ -1068,7 +1069,6 @@ def test_delete_content_marked_deletable__directory_entries(org_app, handlers):
grundeigentumer_in='Berta Bertinio',
publication_start=datetime(2024, 4, 1, tzinfo=tz),
publication_end=datetime(2024, 4, 10, tzinfo=tz),
# delete_when_expired=True,
))
event.delete_when_expired = True

Expand Down Expand Up @@ -1283,3 +1283,106 @@ def count_occurrences():
client.get(get_cronjob_url(job))
assert count_events() == 0
assert count_occurrences() == 0


def test_send_email_notification_for_recent_directory_entry_publications(
org_app,
handlers
):
register_echo_handler(handlers)
register_directory_handler(handlers)

client = Client(org_app)
job = get_cronjob_by_name(org_app, 'hourly_maintenance_tasks')
job.app = org_app
tz = ensure_timezone('Europe/Zurich')

assert len(os.listdir(client.app.maildir)) == 0

transaction.begin()

directories = DirectoryCollection(org_app.session(), type='extended')
planauflage = directories.add(
title='Öffentliche Planauflage',
structure="""
Gesuchsteller/in *= ___
Grundeigentümer/in *= ___
""",
configuration=DirectoryConfiguration(
title="[Gesuchsteller/in]",
order=('Gesuchsteller/in'),
),
enable_update_notifications=True,
)

planauflage.add(values=dict(
gesuchsteller_in='Carmine Carminio',
grundeigentumer_in='Doris Dorinio',
publication_start=datetime(2025, 1, 6, 2, 0, tzinfo=tz),
publication_end=datetime(2025, 1, 30, 2, 0, tzinfo=tz),
))

planauflage.add(values=dict(
gesuchsteller_in='Emil Emilio',
grundeigentumer_in='Franco Francinio',
publication_start=datetime(2025, 1, 8, 2, 0, tzinfo=tz),
publication_end=datetime(2025, 1, 31, 2, 0, tzinfo=tz),
))

EntryRecipientCollection(org_app.session()).add(
directory_id=planauflage.id,
address='[email protected]',
confirmed=True
)

transaction.commit()
close_all_sessions()

def planauflagen():
return (DirectoryCollection(org_app.session(), type='extended')
.by_name('offentliche-planauflage'))

def count_recipients():
return (EntryRecipientCollection(org_app.session()).query()
.filter_by(directory_id=planauflagen().id)
.filter_by(confirmed=True).count())

assert count_recipients() == 1
john = EntryRecipientCollection(org_app.session()).query().first()

with freeze_time(datetime(2025, 1, 1, 4, 0, tzinfo=tz)):
client.get(get_cronjob_url(job))

entry_1 = planauflagen().entries[0]
entry_2 = planauflagen().entries[1]
assert entry_1.notification_sent is None
assert entry_2.notification_sent is None
assert len(os.listdir(client.app.maildir)) == 0

with freeze_time(datetime(2025, 1, 6, 4, 0, tzinfo=tz)):
client.get(get_cronjob_url(job))

entry_1 = planauflagen().entries[0]
entry_2 = planauflagen().entries[1]
assert entry_1.notification_sent
assert entry_2.notification_sent is None

assert len(os.listdir(client.app.maildir)) == 1
message = client.get_email(0)
assert message['To'] == john.address
assert planauflagen().title in message['Subject']
assert entry_1.name in message['TextBody']

with freeze_time(datetime(2025, 1, 8, 4, 0, tzinfo=tz)):
client.get(get_cronjob_url(job))

entry_1 = planauflagen().entries[0]
entry_2 = planauflagen().entries[1]
assert entry_1.notification_sent
assert entry_2.notification_sent

assert len(os.listdir(client.app.maildir)) == 2
message = client.get_email(1)
assert message['To'] == john.address
assert planauflagen().title in message['Subject']
assert entry_2.name in message['TextBody']

0 comments on commit ce6222d

Please sign in to comment.