Repository: liqd/a4-meinberlin

Issue:
notification sent for modules in de/unpublished project
**URL:** (depending on follow status, so URL does not help here)
**user:** follower
**expected behaviour:** I should not get a notification for modules in unpublished projects
**behaviour:** If I follow an unpublished or depublished project (e.g. because a project had been published and after a participation was depublished or because the initiator followed in preview before publishing) I get a notification eg for start of participation. The Module has to be added to project.
**important screensize:**
**device & browser:** 
**Comment/Question:** it is an edge case but was topic in two support questions

Screenshot?



Discussion:
Changed from enhancement to question as I don't know if this is an issue or rather a story. As it is an edge case it may not be broken but always behaved like this?
as @jostertag tested, the same also happens for a+ (@lthueer). So it would be good to investigate what the problem is and decide on issue or story.
I set prio to medium and put into milestone, not to fix it till then but to investigate on know how to proceed.
I think it did always behave like that, but if we need to change it, we can. What should happen though? And which notifications were sent? It is only about the time ones, right? Not about sth. was added to the project?
So, when a project is not published there shouldn't be any notifications about phases starting or ending or offline events taking place, right? That should be easy enough to do. 
We need to check the code to make sure they are still sent when the project is only shortly depublished and then published again, but I think that is also fine.
So, but we shouldn't fix now, but you give us the go? @CarolingerSeilchenspringer ? Just remove the question and decision needed labels if this is good to be fixed.
@fuzzylogic2000 if it is easy and we can do it as an issue but the prio remains low, so need for it to be in next prod release (but can be if you have time). And yes, exclude start phase, end phase and upcoming event from notifications as long as project is draft.

Resolving patch:
diff --git a/meinberlin/apps/notifications/emails.py b/meinberlin/apps/notifications/emails.py
--- a/meinberlin/apps/notifications/emails.py
+++ b/meinberlin/apps/notifications/emails.py
@@ -130,10 +130,8 @@ def get_receivers(self):
         return receivers
 
 
-class NotifyFollowersOnUpcommingEventEmail(Email):
-    template_name = (
-        "meinberlin_notifications/emails" "/notify_followers_event_upcomming"
-    )
+class NotifyFollowersOnUpcomingEventEmail(Email):
+    template_name = "meinberlin_notifications/emails/notify_followers_event_upcoming"
 
     def get_receivers(self):
         action = self.object
diff --git a/meinberlin/apps/notifications/signals.py b/meinberlin/apps/notifications/signals.py
--- a/meinberlin/apps/notifications/signals.py
+++ b/meinberlin/apps/notifications/signals.py
@@ -31,7 +31,7 @@ def send_notifications(instance, created, **kwargs):
             emails.NotifyFollowersOnPhaseIsOverSoonEmail.send(action)
 
     elif action.type == "offlineevent" and verb == Verbs.START:
-        emails.NotifyFollowersOnUpcommingEventEmail.send(action)
+        emails.NotifyFollowersOnUpcomingEventEmail.send(action)
 
 
 @receiver(dashboard_signals.project_created)
diff --git a/meinberlin/apps/offlineevents/management/commands/create_offlineevent_system_actions.py b/meinberlin/apps/offlineevents/management/commands/create_offlineevent_system_actions.py
--- a/meinberlin/apps/offlineevents/management/commands/create_offlineevent_system_actions.py
+++ b/meinberlin/apps/offlineevents/management/commands/create_offlineevent_system_actions.py
@@ -24,7 +24,9 @@ def handle(self, *args, **options):
     def _event_starting(self):
         event_ct = ContentType.objects.get_for_model(OfflineEvent)
 
-        events = OfflineEvent.objects.starts_within(hours=self.event_starting_hours)
+        events = OfflineEvent.objects.starts_within(
+            hours=self.event_starting_hours
+        ).exclude(project__is_draft=True)
         for event in events:
             existing_action = Action.objects.filter(
                 project=event.project,


Test patch:
diff --git a/meinberlin/apps/notifications/management/commands/send_test_emails.py b/meinberlin/apps/notifications/management/commands/send_test_emails.py
--- a/meinberlin/apps/notifications/management/commands/send_test_emails.py
+++ b/meinberlin/apps/notifications/management/commands/send_test_emails.py
@@ -205,7 +205,7 @@ def _send_notification_offlineevent(self):
         TestEmail.send(
             action,
             receiver=[self.user],
-            template_name=notification_emails.NotifyFollowersOnUpcommingEventEmail.template_name,
+            template_name=notification_emails.NotifyFollowersOnUpcomingEventEmail.template_name,
         )
         action.delete()
 
diff --git a/tests/notifications/conftest.py b/tests/notifications/conftest.py
new file mode 100644
--- /dev/null
+++ b/tests/notifications/conftest.py
@@ -0,0 +1,7 @@
+from pytest_factoryboy import register
+
+from meinberlin.test.factories import budgeting as budgeting_factories
+from meinberlin.test.factories import offlineevents
+
+register(budgeting_factories.ProposalFactory)
+register(offlineevents.OfflineEventFactory)
diff --git a/tests/notifications/test_emails.py b/tests/notifications/test_emails.py
new file mode 100644
--- /dev/null
+++ b/tests/notifications/test_emails.py
@@ -0,0 +1,132 @@
+from datetime import timedelta
+
+import pytest
+from dateutil.parser import parse
+from django.contrib.contenttypes.models import ContentType
+from django.core import mail
+from django.core.management import call_command
+from freezegun import freeze_time
+
+from adhocracy4.actions.models import Action
+from adhocracy4.actions.verbs import Verbs
+from adhocracy4.phases.models import Phase
+from adhocracy4.test.helpers import freeze_phase
+from adhocracy4.test.helpers import setup_phase
+from meinberlin.apps.budgeting import phases
+from meinberlin.apps.offlineevents.models import OfflineEvent
+from meinberlin.config import settings
+
+START = Verbs.START.value
+
+EVENT_STARTING_HOURS = 0
+if hasattr(settings, "ACTIONS_OFFLINE_EVENT_STARTING_HOURS"):
+    EVENT_STARTING_HOURS = settings.ACTIONS_OFFLINE_EVENT_STARTING_HOURS
+else:
+    EVENT_STARTING_HOURS = 72
+
+
+@pytest.mark.django_db
+def test_event_soon_email(offline_event_factory):
+    EVENT_DATE = parse("2020-01-05 17:00:00 UTC")
+    ACTION_DATE = EVENT_DATE - timedelta(hours=EVENT_STARTING_HOURS)
+    CURRENT_DATE = ACTION_DATE + timedelta(minutes=30)
+
+    content_type = ContentType.objects.get_for_model(OfflineEvent)
+    offline_event_factory(
+        date=EVENT_DATE,
+    )
+
+    action_count = Action.objects.filter(
+        verb=START, obj_content_type=content_type
+    ).count()
+    assert action_count == 0
+
+    with freeze_time(CURRENT_DATE):
+        call_command("create_offlineevent_system_actions")
+        # NotifyFollowersOnUpcomingEventEmail
+        assert len(mail.outbox) == 1
+        assert mail.outbox[0].subject.startswith("Einladung zu einer Veranstaltung")
+
+
+@pytest.mark.django_db
+def test_event_soon_draft_no_email(offline_event_factory):
+    EVENT_DATE = parse("2020-01-05 17:00:00 UTC")
+    ACTION_DATE = EVENT_DATE - timedelta(hours=EVENT_STARTING_HOURS)
+    CURRENT_DATE = ACTION_DATE + timedelta(minutes=30)
+
+    content_type = ContentType.objects.get_for_model(OfflineEvent)
+    event = offline_event_factory(
+        date=EVENT_DATE,
+    )
+    project = event.project
+    project.is_draft = True
+    project.save()
+    project.refresh_from_db()
+
+    action_count = Action.objects.filter(
+        verb=START, obj_content_type=content_type
+    ).count()
+    assert action_count == 0
+
+    with freeze_time(CURRENT_DATE):
+        call_command("create_offlineevent_system_actions")
+        action_count = Action.objects.filter(
+            verb=START, obj_content_type=content_type
+        ).count()
+        assert action_count == 0
+        # NotifyFollowersOnUpcomingEventEmail
+        assert len(mail.outbox) == 0
+
+
+@pytest.mark.django_db
+def test_phase_started_email(apiclient, phase_factory, proposal_factory):
+    phase, module, project, proposal = setup_phase(
+        phase_factory, proposal_factory, phases.VotingPhase
+    )
+    phase.end_date += timedelta(hours=48)
+    phase.save()
+    phase.refresh_from_db()
+
+    content_type = ContentType.objects.get_for_model(Phase)
+    action_count = Action.objects.filter(
+        verb=START, obj_content_type=content_type
+    ).count()
+    assert action_count == 0
+
+    with freeze_phase(phase):
+        call_command("create_system_actions")
+        action_count = Action.objects.filter(
+            verb=START, obj_content_type=content_type
+        ).count()
+        assert action_count == 1
+        assert len(mail.outbox) == 1
+        assert mail.outbox[0].subject.startswith("Los geht's:")
+
+
+@pytest.mark.django_db
+def test_phase_started_draft_no_email(apiclient, phase_factory, proposal_factory):
+    phase, module, project, proposal = setup_phase(
+        phase_factory, proposal_factory, phases.VotingPhase
+    )
+    project = phase.module.project
+    project.is_draft = True
+    project.save()
+    project.refresh_from_db()
+
+    phase.end_date += timedelta(hours=48)
+    phase.save()
+    phase.refresh_from_db()
+
+    content_type = ContentType.objects.get_for_model(Phase)
+    action_count = Action.objects.filter(
+        verb=START, obj_content_type=content_type
+    ).count()
+    assert action_count == 0
+
+    with freeze_phase(phase):
+        call_command("create_system_actions")
+        action_count = Action.objects.filter(
+            verb=START, obj_content_type=content_type
+        ).count()
+        assert action_count == 0
+        assert len(mail.outbox) == 0