Skip to content
Open
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
6 changes: 6 additions & 0 deletions collectives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
from flask_login import LoginManager, current_user
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect
from itsdangerous import URLSafeTimedSerializer

from collectives import api, forms, models
from collectives.models import Configuration, DBAdaptedFlaskConfig
from collectives.new_event_notifications import register_cli
from collectives.routes import (
activity_supervison,
administration,
Expand Down Expand Up @@ -79,6 +81,9 @@ def create_app(config_filename="config.py", extra_config=None):
# To get one variable, tape app.config['MY_VARIABLE']

fileConfig(app.config["LOGGING_CONFIGURATION"], disable_existing_loggers=False)
app.extensions["new_event_notification_serializer"] = URLSafeTimedSerializer(
app.config["SECRET_KEY"], salt="new-event-notifications"
)

# Initialize plugins
models.db.init_app(app)
Expand Down Expand Up @@ -144,6 +149,7 @@ def create_app(config_filename="config.py", extra_config=None):

forms.configure_forms(app)
forms.csrf.init_app(app)
register_cli(app)

return app

Expand Down
35 changes: 11 additions & 24 deletions collectives/email_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,27 @@
db,
)
from collectives.models.auth import ConfirmationTokenType, TokenEmailStatus
from collectives.new_event_notifications import (
queue_new_event_notification,
send_supervisor_new_event_notification,
)
from collectives.utils import mail
from collectives.utils.time import current_time, format_date
from collectives.utils.url import slugify


def send_new_event_notification(event):
"""Send a notification to activity supervisor when a new event is created
"""Queue digest notifications and notify configured activity emails immediately.

:param event: The new created event.
:type event: :py:class:`collectives.modes.event.Event`
"""
emails = [a.email for a in event.activity_types if a.email is not None]
if emails:
leader_names = [l.full_name() for l in event.leaders]
activity_names = [a.name for a in event.activity_types]
subject = Configuration.NEW_EVENT_SUBJECT
if event.status == EventStatus.Pending:
subject = f"{subject} ({EventStatus.display_names()[EventStatus.Pending]})"
message = Configuration.NEW_EVENT_MESSAGE.format(
leader_name=",".join(leader_names),
activity_name=",".join(activity_names),
event_title=event.title,
link=url_for(
"event.view_event",
event_id=event.id,
name=slugify(event.title),
_external=True,
),
)
try:
mail.send_mail(subject=subject, email=emails, message=message)
# pylint: disable=broad-except
except BaseException as err:
current_app.logger.error(f"Mailer error: {err}")
queue_new_event_notification(event)
try:
send_supervisor_new_event_notification(event)
# pylint: disable=broad-except
except BaseException as err:
current_app.logger.error(f"Mailer error: {err}")


def send_unregister_notification(event: Event, user: User, reason: str):
Expand Down
1 change: 1 addition & 0 deletions collectives/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
AdminUserForm,
ExtranetUserForm,
LocalUserForm,
NotificationPreferencesForm,
RoleForm,
)
from collectives.models import avatars, image_equipment_type, photos
Expand Down
117 changes: 116 additions & 1 deletion collectives/forms/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@

from collectives.forms.activity_type import ActivityTypeSelectionForm
from collectives.forms.order import OrderedModelForm
from collectives.forms.utils import coerce_optional
from collectives.forms.utils import MultiCheckboxField, coerce_optional
from collectives.forms.validators import (
LicenseValidator,
PasswordValidator,
UniqueValidator,
)
from collectives.models import (
ActivityKind,
ActivityType,
EventType,
NotificationFrequency,
Role,
RoleIds,
User,
Expand Down Expand Up @@ -95,6 +98,15 @@ def __init__(self, *args, **kwargs):
AvatarForm.__init__(self, kwargs.get("obj"))


class AdminTestUserCreationForm(AdminTestUserForm):
"""Form for admins to create test/local users without notification internals."""

class Meta(AdminTestUserForm.Meta):
"""Fields to expose on creation."""

exclude = [*AdminTestUserForm.Meta.exclude, "new_event_notification_enabled", "new_event_notification_weekdays", "new_event_notification_frequency", "last_new_event_notification_sent_at", "last_new_event_notification_clicked_at", "new_event_notification_warning_sent_at"]


class AdminUserForm(OrderedModelForm, AvatarForm):
"""Form for admins to edit real users info"""

Expand Down Expand Up @@ -237,3 +249,106 @@ def validate_license(self, field: StringField):

if field.data.strip() != self._user.license:
raise ValidationError("Le numéro de license ne correspond pas")


class NotificationPreferencesForm(FlaskForm):
"""Form for managing event creation notification preferences."""

WEEKDAY_CHOICES = [
(0, "Lundi"),
(1, "Mardi"),
(2, "Mercredi"),
(3, "Jeudi"),
(4, "Vendredi"),
(5, "Samedi"),
(6, "Dimanche"),
]

new_event_notification_enabled = BooleanField(
"Recevoir des notifications de nouvelles collectives",
description=(
"Si activé, vous recevrez un e-mail groupé quand des collectives "
"correspondent aux filtres ci-dessous."
),
)
new_event_notification_frequency = SelectField(
"Fréquence d'envoi",
coerce=NotificationFrequency.coerce,
choices=NotificationFrequency.choices(),
description="Choisissez un récapitulatif quotidien ou hebdomadaire.",
)
event_type_ids = MultiCheckboxField(
"Types d'événement",
coerce=int,
description="Laisser vide pour tous les types.",
)
activity_type_ids = MultiCheckboxField(
"Activités",
coerce=int,
description="Laisser vide pour toutes les activités.",
)
weekdays = MultiCheckboxField(
"Jours de la semaine",
choices=WEEKDAY_CHOICES,
coerce=int,
description="Laisser vide pour tous les jours.",
)
next = HiddenField()
submit = SubmitField("Enregistrer")

def __init__(self, user: User, *args, **kwargs):
super().__init__(*args, **kwargs)
self._user = user

event_types = EventType.get_all_types()
activity_types = ActivityType.get_all_types()
self.event_type_requires_activity_ids = {
event_type.id for event_type in event_types if event_type.requires_activity
}
self.event_type_icon_names = {
event_type.id: event_type.short for event_type in event_types
}
self.event_type_ids.choices = [
(event_type.id, event_type.name) for event_type in event_types
]
self.activity_type_icon_names = {
activity_type.id: (
activity_type.short
if activity_type.kind == ActivityKind.Regular
else "benevolat"
)
for activity_type in activity_types
}
self.activity_type_ids.choices = [
(activity_type.id, activity_type.name) for activity_type in activity_types
]

if not self.is_submitted():
self.new_event_notification_enabled.data = (
user.new_event_notification_enabled
)
self.new_event_notification_frequency.data = (
user.new_event_notification_frequency
)
self.event_type_ids.data = [
event_type.id for event_type in user.notified_event_types
]
self.activity_type_ids.data = [
activity_type.id for activity_type in user.notified_activity_types
]
self.weekdays.data = user.notification_weekday_list()

def selected_event_types_require_activity(self) -> bool:
"""Whether the current event type selection makes activity filters relevant."""
if not self.event_type_ids.data:
return True
return any(
event_type_id in self.event_type_requires_activity_ids
for event_type_id in self.event_type_ids.data
)

def normalized_activity_type_ids(self) -> list[int]:
"""Return only activity filters that are compatible with selected event types."""
if self.event_type_ids.data and not self.selected_event_types_require_activity():
return []
return self.activity_type_ids.data or []
9 changes: 8 additions & 1 deletion collectives/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
)
from collectives.models.event_tag import EventTag
from collectives.models.globals import db
from collectives.models.new_event_notification import NewEventNotification
from collectives.models.payment import (
ItemPrice,
Payment,
Expand All @@ -50,7 +51,13 @@
)
from collectives.models.role import Role, RoleIds
from collectives.models.upload import UploadedFile, documents
from collectives.models.user import Gender, User, UserType, avatars
from collectives.models.user import (
Gender,
NotificationFrequency,
User,
UserType,
avatars,
)
from collectives.models.user_group import (
GroupEventCondition,
GroupLicenseCondition,
Expand Down
21 changes: 21 additions & 0 deletions collectives/models/new_event_notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Persistence for pending new-event notification digests."""

from collectives.models.globals import db
from collectives.utils.time import current_time


class NewEventNotification(db.Model):
"""Track newly created events that may appear in digests."""

__tablename__ = "new_event_notifications"

id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(
db.Integer,
db.ForeignKey("events.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
created_at = db.Column(db.DateTime, nullable=False, default=current_time, index=True)

event = db.relationship("Event", lazy="joined")
2 changes: 1 addition & 1 deletion collectives/models/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from collectives.models.globals import db
from collectives.models.role import Role, RoleIds
from collectives.models.user.badge import UserBadgeMixin
from collectives.models.user.enum import Gender, UserType
from collectives.models.user.enum import Gender, NotificationFrequency, UserType
from collectives.models.user.misc import UserMiscMixin, avatars
from collectives.models.user.model import UserModelMixin
from collectives.models.user.role import UserRoleMixin
Expand Down
17 changes: 17 additions & 0 deletions collectives/models/user/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,20 @@ def display_names(cls):
cls.Local: "Local",
cls.UnverifiedLocal: "Local (email non-vérifié)",
}


class NotificationFrequency(ChoiceEnum):
"""Enum to store new-event notification digest frequency."""

# pylint: disable=invalid-name
Daily = 0
Weekly = 1
# pylint: enable=invalid-name

@classmethod
def display_names(cls):
""":return: Human-readable notification frequency labels."""
return {
cls.Daily: "Quotidien",
cls.Weekly: "Hebdomadaire",
}
Loading