Switch to Group-based reminder subscriptions, add tool subscriptions

This commit is contained in:
Adam Goldsmith 2020-12-17 11:47:22 -05:00
parent 365c97d0ef
commit fb0b5be914
6 changed files with 113 additions and 50 deletions

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Tool, Task, Reminder, Event from .models import Tool, Task, Event, GroupTaskSubscription, GroupToolSubscription
admin.site.register(Tool) admin.site.register(Tool)
@ -10,5 +10,6 @@ class TaskAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
admin.site.register(Reminder) admin.site.register(GroupTaskSubscription)
admin.site.register(GroupToolSubscription)
admin.site.register(Event) admin.site.register(Event)

View File

@ -4,38 +4,54 @@ from django.core.mail import send_mail
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.template import loader 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): class Command(BaseCommand):
help = 'Sends any notifications for upcoming and overdue tasks' help = 'Sends any notifications for upcoming and overdue tasks'
def _active_reminders(self): def _active_task_subscriptions(self):
for reminder in Reminder.objects.all(): for subscription in GroupTaskSubscription.objects.all():
if reminder.should_remind: if subscription.should_remind:
yield reminder yield subscription
def handle(self, *args, **options): for tool_subscription in GroupToolSubscription.objects.all():
template = loader.get_template('tasks/notificationEmail.txt') for subscription in tool_subscription.get_task_subscriptions():
if subscription.should_remind:
yield subscription
reminders_per_user = { def _expand_group_subscriptions_to_users(self, subscriptions):
user: sorted(reminders, key=lambda r: r.task.next_recurrence) out = {}
for user, reminders in groupby(self._active_reminders(), lambda r: r.user) 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: if not user.email:
self.stdout.write(self.style.ERROR( self.stdout.write(self.style.ERROR(
f"Can't send email, user '{user}' is missing an email address")) f"Can't send email, user '{user}' is missing an email address"))
continue continue
self.stdout.write(self.style.SUCCESS( 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: try:
send_mail( send_mail(
subject=f'[CMS Tool Maintenance] {len(reminders)} tasks are upcoming or overdue!', subject=f'[CMS Tool Maintenance] {len(subscriptions)} tasks are upcoming or overdue!',
message=template.render({'reminders': reminders}).strip(), message=template.render({'subscriptions': subscriptions}).strip(),
from_email='adam@adamgoldsmith.name', from_email='adam@adamgoldsmith.name',
recipient_list=[user.email], recipient_list=[user.email],
fail_silently=False fail_silently=False

View File

@ -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')},
},
),
]

View File

@ -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)),
],
),
]

View File

@ -2,6 +2,7 @@ from datetime import datetime
from dateutil.rrule import rrulestr from dateutil.rrule import rrulestr
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -58,22 +59,53 @@ class Task(models.Model):
return next_rec < datetime.now() return next_rec < datetime.now()
class Reminder(models.Model): class SubscriptionSettings(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) days_before = models.PositiveIntegerField()
task = models.ForeignKey(Task, on_delete=models.CASCADE)
days_before = models.IntegerField() 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: class Meta:
# Django doesn't support multiple-column primary keys # 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 @property
def should_remind(self): 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) return self.task.is_overdue or (time_until_overdue.days <= self.days_before)
def __str__(self): 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): class Event(models.Model):

View File

@ -1,4 +1,4 @@
The following tasks are upcoming or overdue: The following tasks are upcoming or overdue:
{% for reminder in reminders %} {% for subscription in subscriptions %}
- {{ reminder.task.name }} {{ reminder.task.next_recurrence|date }} - {{ subscription.task.name }} {{ subscription.task.next_recurrence|date }}
{% endfor %} {% endfor %}