Skip to content

Commit

Permalink
Merge pull request #579 from maykinmedia/feature/1328-checkbox-in-pro…
Browse files Browse the repository at this point in the history
…file-for-enabling-disabling-notifications

[#1328] Add checkboxes for cases, plans, messages notifications to Profile page
  • Loading branch information
alextreme authored Apr 18, 2023
2 parents eb1d59a + f56cb31 commit 0bd2642
Show file tree
Hide file tree
Showing 22 changed files with 337 additions and 19 deletions.
11 changes: 11 additions & 0 deletions src/open_inwoner/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ class _UserAdmin(ImageCroppingMixin, UserAdmin):
),
},
),
(
_("Notifications"),
{
"classes": ("collapse",),
"fields": (
"cases_notifications",
"messages_notifications",
"plans_notifications",
),
},
),
(
_("Contacts - invites"),
{"fields": ("user_contacts", "contacts_for_approval")},
Expand Down
16 changes: 16 additions & 0 deletions src/open_inwoner/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ def __init__(self, *args, **kwargs) -> None:
self.fields["selected_categories"].queryset = Category.objects.published()


class UserNotificationsForm(forms.ModelForm):
class Meta:
model = User
fields = (
"cases_notifications",
"messages_notifications",
"plans_notifications",
)

def __init__(self, user, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

if not user.login_type == LoginTypeChoices.digid:
del self.fields["cases_notifications"]


class ContactFilterForm(forms.Form):
type = forms.ChoiceField(
label=_("Type contact"), choices=EmptyContactTypeChoices.choices, required=False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@


class Command(BaseCommand):
help = "Send emails about new messages to the users"
help = "Send emails about expiring actions to the users"

def handle(self, *args, **options):
today = date.today()
user_ids = Action.objects.filter(end_date=today).values_list(
"is_for_id", flat=True
)
receivers = User.objects.filter(is_active=True, pk__in=user_ids).distinct()
receivers = User.objects.filter(
is_active=True, pk__in=user_ids, plans_notifications=True
).distinct()

for receiver in receivers:
"""send email to each user"""
Expand All @@ -32,7 +34,7 @@ def handle(self, *args, **options):
)

logger.info(
f"The email was send to the user {receiver} about {actions.count()} expiring actions"
f"The email was sent to the user {receiver} about {actions.count()} expiring actions"
)

def send_email(self, receiver: User, actions: List[Action]):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import logging

from django.conf import settings
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
from django.db.models import Count
from django.urls import reverse
Expand All @@ -22,6 +20,7 @@ def handle(self, *args, **options):
received_messages__seen=False,
received_messages__sent=False,
is_active=True,
messages_notifications=True,
).distinct()

for receiver in receivers:
Expand All @@ -45,7 +44,7 @@ def handle(self, *args, **options):
messages_to_update.update(sent=True)

logger.info(
f"The email was send to the user {receiver} about {total_messages} new messages"
f"The email was sent to the user {receiver} about {total_messages} new messages"
)

def send_email(self, receiver: User, total_senders: int, total_messages: int):
Expand Down
40 changes: 40 additions & 0 deletions src/open_inwoner/accounts/migrations/0059_auto_20230412_1637.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 3.2.15 on 2023-04-12 14:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("accounts", "0058_alter_user_selected_categories"),
]

operations = [
migrations.AddField(
model_name="user",
name="cases_notifications",
field=models.BooleanField(
default=True,
help_text="Indicates if the user wants to receive notifications for updates concerning cases.",
verbose_name="Cases notifications",
),
),
migrations.AddField(
model_name="user",
name="messages_notifications",
field=models.BooleanField(
default=True,
help_text="Indicates if the user wants to receive notifications for new messages.",
verbose_name="Messages notifications",
),
),
migrations.AddField(
model_name="user",
name="plans_notifications",
field=models.BooleanField(
default=True,
help_text="Indicates if the user wants to receive notifications for updates concerning plans and actions.",
verbose_name="Plans notifications",
),
),
]
34 changes: 34 additions & 0 deletions src/open_inwoner/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ class User(AbstractBaseUser, PermissionsMixin):
blank=True,
help_text="This field indicates if a user signed up with OpenId Connect or not.",
)
cases_notifications = models.BooleanField(
verbose_name=_("Cases notifications"),
default=True,
help_text=_(
"Indicates if the user wants to receive notifications for updates concerning cases."
),
)
messages_notifications = models.BooleanField(
verbose_name=_("Messages notifications"),
default=True,
help_text=_(
"Indicates if the user wants to receive notifications for new messages."
),
)
plans_notifications = models.BooleanField(
verbose_name=_("Plans notifications"),
default=True,
help_text=_(
"Indicates if the user wants to receive notifications for updates concerning plans and actions."
),
)
user_contacts = models.ManyToManyField(
"self",
verbose_name=_("Contacts"),
Expand Down Expand Up @@ -227,6 +248,19 @@ def get_interests(self) -> str:

return ", ".join(list(self.selected_categories.values_list("name", flat=True)))

def get_active_notifications(self) -> str:
enabled = []
if self.cases_notifications and self.login_type == LoginTypeChoices.digid:
enabled.append(_("cases"))
if self.messages_notifications:
enabled.append(_("messages"))
if self.plans_notifications:
enabled.append(_("plans"))
if not enabled:
return _("You do not have any notifications enabled.")

return ", ".join(str(notification) for notification in enabled)

def require_necessary_fields(self) -> bool:
"""returns whether user needs to fill in necessary fields"""
if (
Expand Down
6 changes: 6 additions & 0 deletions src/open_inwoner/accounts/tests/test_actions_expire.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def test_notify_about_expiring_action(self):
self.assertIn(action.name, html_body)
self.assertIn(reverse("accounts:action_list"), html_body)

def test_no_notification_about_expiring_action_when_disabled(self):
user = UserFactory(plans_notifications=False)
action = ActionFactory(end_date=date.today(), created_by=user)
call_command("actions_expire")
self.assertEqual(len(mail.outbox), 0)

def test_action_does_not_expire_yet(self):
user = UserFactory()
ActionFactory(end_date=date.today() + timedelta(days=1), created_by=user)
Expand Down
21 changes: 21 additions & 0 deletions src/open_inwoner/accounts/tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ def test_categories_modification_is_logged(self):
},
)

def test_user_notifications_update_is_logged(self):
form = self.app.get(reverse("accounts:my_notifications"), user=self.user).forms[
"change-notifications"
]
form["messages_notifications"] = False
form.submit()
log_entry = TimelineLog.objects.last()

self.assertEqual(
log_entry.timestamp.strftime("%m/%d/%Y, %H:%M:%S"), "10/18/2021, 13:00:00"
)
self.assertEqual(log_entry.content_object.id, self.user.id)
self.assertEqual(
log_entry.extra_data,
{
"message": _("users notifications were modified"),
"action_flag": list(LOG_ACTIONS[CHANGE]),
"content_object_repr": self.user.email,
},
)

def test_login_via_admin_is_logged(self):
self.app.post(reverse("admin:login"), user=self.user)
log_entry = TimelineLog.objects.get()
Expand Down
15 changes: 15 additions & 0 deletions src/open_inwoner/accounts/tests/test_notify_about_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ def test_notify_about_received_message(self):
message.refresh_from_db()
self.assertTrue(message.sent)

def test_no_notification_about_received_message_when_disabled_by_user(self):
user = UserFactory(messages_notifications=False)
sender = UserFactory()
message = MessageFactory.create(receiver=user, sender=sender)

self.assertFalse(message.sent)

call_command("notify_about_messages")

self.assertEqual(len(mail.outbox), 0)

message.refresh_from_db()

self.assertFalse(message.sent)

def test_seen_message(self):
user = UserFactory.create()

Expand Down
54 changes: 54 additions & 0 deletions src/open_inwoner/accounts/tests/test_profile_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ def test_mydata_not_shown_without_digid(self):
response = self.app.get(self.url, user=self.user)
self.assertNotContains(response, _("Mijn gegevens"))

def test_active_user_notifications_are_shown(self):
response = self.app.get(self.url, user=self.user)
self.assertContains(response, _("messages, plans"))

def test_expected_message_is_shown_when_all_notifications_disabled(self):
self.user.cases_notifications = False
self.user.messages_notifications = False
self.user.plans_notifications = False
self.user.save()
response = self.app.get(self.url, user=self.user)
self.assertContains(response, _("You do not have any notifications enabled."))


class EditProfileTests(WebTest):
def setUp(self):
Expand Down Expand Up @@ -539,6 +551,48 @@ def test_preselected_values(self):
self.assertFalse(form.get("selected_categories", index=2).checked)


class EditNotificationsTests(WebTest):
def setUp(self):
self.url = reverse("accounts:my_notifications")
self.user = UserFactory()

def test_login_required(self):
login_url = reverse("login")
response = self.app.get(self.url)

self.assertRedirects(response, f"{login_url}?next={self.url}")

def test_default_values_for_regular_user(self):
response = self.app.get(self.url, user=self.user)
form = response.forms["change-notifications"]

self.assertTrue(form.get("messages_notifications").checked)
self.assertTrue(form.get("plans_notifications").checked)
self.assertNotIn("cases_notifications", form.fields)

def test_disabling_notification_is_saved(self):
self.assertTrue(self.user.messages_notifications)

response = self.app.get(self.url, user=self.user)
form = response.forms["change-notifications"]
form["messages_notifications"] = False
form.submit()

self.user.refresh_from_db()

self.assertTrue(self.user.cases_notifications)
self.assertFalse(self.user.messages_notifications)
self.assertTrue(self.user.plans_notifications)

def test_cases_notifications_is_accessible_when_digid_user(self):
self.user.login_type = LoginTypeChoices.digid
self.user.save()
response = self.app.get(self.url, user=self.user)
form = response.forms["change-notifications"]

self.assertIn("cases_notifications", form.fields)


class ExportProfileTests(WebTest):
def setUp(self):
self.url = reverse("accounts:profile_export")
Expand Down
2 changes: 2 additions & 0 deletions src/open_inwoner/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
InviteAcceptView,
MyCategoriesView,
MyDataView,
MyNotificationsView,
MyProfileExportView,
MyProfileView,
NecessaryFieldsUserView,
Expand Down Expand Up @@ -99,6 +100,7 @@
path("contacts/", ContactListView.as_view(), name="contact_list"),
path("onderwerpen/", MyCategoriesView.as_view(), name="my_categories"),
path("mydata/", MyDataView.as_view(), name="my_data"),
path("notifications/", MyNotificationsView.as_view(), name="my_notifications"),
path("cases/open/", OpenCaseListView.as_view(), name="my_open_cases"),
path("cases/closed/", ClosedCaseListView.as_view(), name="my_closed_cases"),
path("cases/forms/", OpenSubmissionListView.as_view(), name="open_submissions"),
Expand Down
1 change: 1 addition & 0 deletions src/open_inwoner/accounts/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
EditProfileView,
MyCategoriesView,
MyDataView,
MyNotificationsView,
MyProfileExportView,
MyProfileView,
)
Expand Down
32 changes: 31 additions & 1 deletion src/open_inwoner/accounts/views/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from open_inwoner.utils.mixins import ExportMixin
from open_inwoner.utils.views import CommonPageMixin, LogMixin

from ..forms import BrpUserForm, CategoriesForm, UserForm
from ..forms import BrpUserForm, CategoriesForm, UserForm, UserNotificationsForm
from ..models import Action, User


Expand Down Expand Up @@ -228,6 +228,36 @@ def parse_brp_data(self):
return my_data


class MyNotificationsView(
LogMixin, LoginRequiredMixin, CommonPageMixin, BaseBreadcrumbMixin, UpdateView
):
template_name = "pages/profile/notifications.html"
model = User
form_class = UserNotificationsForm
success_url = reverse_lazy("accounts:my_profile")

@cached_property
def crumbs(self):
return [
(_("Mijn profiel"), reverse("accounts:my_profile")),
(_("Communicatievoorkeuren"), reverse("accounts:my_notifications")),
]

def get_object(self):
return self.request.user

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs

def form_valid(self, form):
form.save()

self.log_change(self.object, _("users notifications were modified"))
return HttpResponseRedirect(self.get_success_url())


class MyProfileExportView(LogMixin, LoginRequiredMixin, ExportMixin, DetailView):
template_name = "export/profile/profile_export.html"
model = User
Expand Down
Loading

0 comments on commit 0bd2642

Please sign in to comment.