Skip to content

Commit

Permalink
Directory: Enable option for getting notifications on new directory e…
Browse files Browse the repository at this point in the history
…ntries

If option is enabled in directory settings, people can now subscribe to a directory. Whenever said directory gets a new entry, subscribers get a notification email.

TYPE: Feature
LINK: OGC-1595
  • Loading branch information
BreathingFlesh authored Jun 10, 2024
1 parent 6f7202c commit 56de8c2
Show file tree
Hide file tree
Showing 26 changed files with 1,204 additions and 162 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ repos:
- id: sass-lint
files: '^src/.*\.scss'
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.3.0
rev: v9.4.0
hooks:
- id: eslint
files: '^src/.*\.jsx?$'
Expand Down
39 changes: 38 additions & 1 deletion src/onegov/directory/collections/directory.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from onegov.core.collection import GenericCollection
from onegov.core.utils import normalize_for_url, increment_name
from onegov.core.utils import normalize_for_url, increment_name, is_uuid
from onegov.directory.models import Directory
from onegov.directory.models.directory import EntryRecipient
from onegov.directory.types import DirectoryConfiguration


from typing import overload, Any, Literal, TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from sqlalchemy.orm import Query, Session
from uuid import UUID


DirectoryT = TypeVar('DirectoryT', bound=Directory)
Expand Down Expand Up @@ -70,3 +72,38 @@ def unique_name(self, title: str) -> str:

def by_name(self, name: str) -> DirectoryT | None:
return self.query().filter_by(name=name).first()


class EntryRecipientCollection:

def __init__(self, session: 'Session'):
self.session = session

def query(self) -> 'Query[EntryRecipient]':
return self.session.query(EntryRecipient)

def by_id(self, id: 'str | UUID') -> EntryRecipient | None:
if is_uuid(id):
return self.query().filter(EntryRecipient.id == id).first()
return None

def add(
self,
address: str,
directory_id: 'UUID',
confirmed: bool = False
) -> EntryRecipient:

recipient = EntryRecipient(
address=address,
directory_id=directory_id,
confirmed=confirmed
)
self.session.add(recipient)
self.session.flush()

return recipient

def delete(self, recipient: EntryRecipient) -> None:
self.session.delete(recipient)
self.session.flush()
74 changes: 73 additions & 1 deletion src/onegov/directory/models/directory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import inspect

from email_validator import validate_email
from enum import Enum
from onegov.core.cache import instance_lru_cache
from onegov.core.cache import lru_cache
Expand All @@ -17,12 +18,13 @@
from onegov.form import flatten_fieldsets, parse_formcode, parse_form
from onegov.search import SearchableContent
from sedate import to_timezone
from sqlalchemy import Column
from sqlalchemy import Boolean, Column
from sqlalchemy import func, exists, and_
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy.orm import object_session
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy_utils import aggregated
from uuid import uuid4
Expand Down Expand Up @@ -585,3 +587,73 @@ def process_obj(self, obj: 'DirectoryEntry') -> None:
form_field.data = data

return DirectoryEntryForm


class EntryRecipient(Base, TimestampMixin):
""" Represents a single recipient.
"""

__tablename__ = 'entry_recipients'

#: the id of the recipient, used in the url
id: 'Column[uuid.UUID]' = Column(
UUID, # type:ignore[arg-type]
primary_key=True,
default=uuid4
)

#: the email address of the recipient
address: 'Column[str]' = Column(Text, nullable=False)

@validates('address')
def validate_address(self, key: str, address: str) -> str:
assert validate_email(address)
return address

#: this token is used for confirm and unsubscribe
token: 'Column[str]' = Column(Text, nullable=False, default=random_token)

#: when recipients are added, they are unconfirmed. At this point they get
#: one e-mail with a confirmation link. If they ignore said e-mail they
#: should not get another one.
confirmed: 'Column[bool]' = Column(Boolean, nullable=False, default=False)

@property
def subscription(self) -> 'EntrySubscription':
return EntrySubscription(self, self.token)

directory_id: 'Column[uuid.UUID]' = Column(
UUID, # type:ignore[arg-type]
nullable=False
)


class EntrySubscription:
""" Adds subscription management to a recipient. """

def __init__(self, recipient: EntryRecipient, token: str):
self.recipient = recipient
self.token = token

@property
def recipient_id(self) -> 'uuid.UUID':
# even though this seems redundant, we need this property
# for morepath, so it can match it to the path variable
return self.recipient.id

def confirm(self) -> bool:
if self.recipient.token != self.token:
return False

self.recipient.confirmed = True
return True

def unsubscribe(self) -> bool:
if self.recipient.token != self.token:
return False

session = object_session(self.recipient)
session.delete(self.recipient)
session.flush()

return True
19 changes: 19 additions & 0 deletions src/onegov/org/forms/directory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from functools import cached_property

from wtforms import EmailField
from onegov.core.utils import safe_format_keys, normalize_for_url
from onegov.directory import DirectoryConfiguration
from onegov.directory import DirectoryZipArchive
Expand Down Expand Up @@ -28,6 +30,7 @@
from wtforms.fields import TextAreaField
from wtforms.validators import DataRequired
from wtforms.validators import InputRequired
from wtforms.validators import Email
from wtforms.validators import Optional
from wtforms.validators import ValidationError

Expand Down Expand Up @@ -321,6 +324,12 @@ class DirectoryBaseForm(Form):
fieldset=_("Publication"),
default=False)

enable_update_notifications = BooleanField(
label=_("Enable registering for update notifications"),
description=_("Users can register for updates on new entries"),
fieldset=_("Notifications"),
default=False)

required_publication = BooleanField(
label=_("Required publication dates"),
fieldset=_("Publication"),
Expand Down Expand Up @@ -766,3 +775,13 @@ def validate_name(self, field: StringField) -> None:
raise ValidationError(
_("An entry with the same name exists")
)


class DirectoryRecipientForm(Form):
"""Form for adding recipients of entry updates to the directory."""

address = EmailField(
label=_("E-Mail"),
description="[email protected]",
validators=[InputRequired(), Email()]
)
Loading

0 comments on commit 56de8c2

Please sign in to comment.