diff --git a/tasks/admin.py b/tasks/admin.py index 4d246f2..faaa26b 100644 --- a/tasks/admin.py +++ b/tasks/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Tool, Task, Reminder, Event +from .models import Tool, Task, Event, GroupTaskSubscription, GroupToolSubscription admin.site.register(Tool) @@ -10,5 +10,6 @@ class TaskAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} -admin.site.register(Reminder) +admin.site.register(GroupTaskSubscription) +admin.site.register(GroupToolSubscription) admin.site.register(Event) diff --git a/tasks/management/commands/sendNotifications.py b/tasks/management/commands/sendNotifications.py index dd114b8..3a173fb 100644 --- a/tasks/management/commands/sendNotifications.py +++ b/tasks/management/commands/sendNotifications.py @@ -4,38 +4,54 @@ from django.core.mail import send_mail from django.core.management.base import BaseCommand, CommandError from django.template import loader -from tasks.models import Tool, Task, Event, Reminder +from tasks.models import Tool, Task, Event, GroupToolSubscription, GroupTaskSubscription class Command(BaseCommand): help = 'Sends any notifications for upcoming and overdue tasks' - def _active_reminders(self): - for reminder in Reminder.objects.all(): - if reminder.should_remind: - yield reminder + def _active_task_subscriptions(self): + for subscription in GroupTaskSubscription.objects.all(): + if subscription.should_remind: + yield subscription - def handle(self, *args, **options): - template = loader.get_template('tasks/notificationEmail.txt') + for tool_subscription in GroupToolSubscription.objects.all(): + for subscription in tool_subscription.get_task_subscriptions(): + if subscription.should_remind: + yield subscription - reminders_per_user = { - user: sorted(reminders, key=lambda r: r.task.next_recurrence) - for user, reminders in groupby(self._active_reminders(), lambda r: r.user) + def _expand_group_subscriptions_to_users(self, subscriptions): + out = {} + for subscription in subscriptions: + for user in subscription.group.user_set.all(): + if user not in out: + out[user] = [] + out[user].append(subscription) + + return { + user: sorted(subscriptions, key=lambda r: r.task.next_recurrence) + for user, subscriptions in out.items() } - for user, reminders in reminders_per_user.items(): + def handle(self, *args, **options): + template = loader.get_template('tasks/notificationEmail.txt.dtl') + + group_subscriptions = self._active_task_subscriptions() + subscriptions_per_user = self._expand_group_subscriptions_to_users(group_subscriptions) + + for user, subscriptions in subscriptions_per_user.items(): if not user.email: self.stdout.write(self.style.ERROR( f"Can't send email, user '{user}' is missing an email address")) continue self.stdout.write(self.style.SUCCESS( - f'Sending notification for {len(reminders)} task(s) to {user}')) + f'Sending notification for {len(subscriptions)} task(s) to {user}')) try: send_mail( - subject=f'[CMS Tool Maintenance] {len(reminders)} tasks are upcoming or overdue!', - message=template.render({'reminders': reminders}).strip(), + subject=f'[CMS Tool Maintenance] {len(subscriptions)} tasks are upcoming or overdue!', + message=template.render({'subscriptions': subscriptions}).strip(), from_email='adam@adamgoldsmith.name', recipient_list=[user.email], fail_silently=False diff --git a/tasks/migrations/0002_grouptasksubscription_grouptoolsubscription.py b/tasks/migrations/0002_grouptasksubscription_grouptoolsubscription.py new file mode 100644 index 0000000..099626a --- /dev/null +++ b/tasks/migrations/0002_grouptasksubscription_grouptoolsubscription.py @@ -0,0 +1,39 @@ +# Generated by Django 3.1.4 on 2020-12-17 16:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('tasks', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='GroupToolSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('days_before', models.PositiveIntegerField()), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('tool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.tool')), + ], + options={ + 'unique_together': {('group', 'tool')}, + }, + ), + migrations.CreateModel( + name='GroupTaskSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('days_before', models.PositiveIntegerField()), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.task')), + ], + options={ + 'unique_together': {('group', 'task')}, + }, + ), + ] diff --git a/tasks/migrations/0002_reminder.py b/tasks/migrations/0002_reminder.py deleted file mode 100644 index a33dc33..0000000 --- a/tasks/migrations/0002_reminder.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.1.4 on 2020-12-07 21:13 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tasks', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Subscription', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('days_before', models.IntegerField()), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.task')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/tasks/models.py b/tasks/models.py index 89ec706..392c686 100644 --- a/tasks/models.py +++ b/tasks/models.py @@ -2,6 +2,7 @@ from datetime import datetime from dateutil.rrule import rrulestr from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from django.db import models from django.urls import reverse @@ -58,22 +59,53 @@ class Task(models.Model): return next_rec < datetime.now() -class Reminder(models.Model): - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - task = models.ForeignKey(Task, on_delete=models.CASCADE) - days_before = models.IntegerField() +class SubscriptionSettings(models.Model): + days_before = models.PositiveIntegerField() + + class Meta: + abstract = True + + def __str__(self): + return f"{self.days_before} day(s)" + + +class GroupToolSubscription(SubscriptionSettings): + group = models.ForeignKey(Group, on_delete=models.CASCADE) + tool = models.ForeignKey(Tool, on_delete=models.CASCADE) + + def get_task_subscriptions(self): + for task in self.tool.task_set.all(): + yield GroupTaskSubscription( + days_before=self.days_before, + group=self.group, + task=task) class Meta: # Django doesn't support multiple-column primary keys - unique_together = (("user", "task"),) + unique_together = (("group", "tool"),) + + def __str__(self): + return f"{self.group}-{self.tool}, {super().__str__()}" + + +class GroupTaskSubscription(SubscriptionSettings): + group = models.ForeignKey(Group, on_delete=models.CASCADE) + task = models.ForeignKey(Task, on_delete=models.CASCADE) + + class Meta: + # Django doesn't support multiple-column primary keys + unique_together = (("group", "task"),) @property def should_remind(self): - time_until_overdue = self.task.next_recurrence - datetime.now() + next_recurrence = self.task.next_recurrence + if next_recurrence is None: + return False + time_until_overdue = next_recurrence - datetime.now() return self.task.is_overdue or (time_until_overdue.days <= self.days_before) def __str__(self): - return f"{self.user}-{self.task}, {self.days_before} day(s)" + return f"{self.group}-{self.task}, {super().__str__()}" class Event(models.Model): diff --git a/tasks/templates/tasks/notificationEmail.txt.dtl b/tasks/templates/tasks/notificationEmail.txt.dtl index 85ec367..5799fe4 100644 --- a/tasks/templates/tasks/notificationEmail.txt.dtl +++ b/tasks/templates/tasks/notificationEmail.txt.dtl @@ -1,4 +1,4 @@ The following tasks are upcoming or overdue: -{% for reminder in reminders %} - - {{ reminder.task.name }} {{ reminder.task.next_recurrence|date }} +{% for subscription in subscriptions %} + - {{ subscription.task.name }} {{ subscription.task.next_recurrence|date }} {% endfor %}