diff --git a/Pipfile b/Pipfile index 00a726e..3e3b336 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,6 @@ verify_ssl = true name = "pypi" [packages] -python-dateutil = "*" django = "*" django-widget-tweaks = "*" django-auth-ldap = "*" @@ -12,6 +11,7 @@ django-markdownx = "*" django-markdownify = "*" uvicorn = "*" mysqlclient = "*" +django-recurrence = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index c37664c..a7f34c4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cf9669556a14beab13e75133255258d1c446c321e486d27def845667bfaec370" + "sha256": "909b6de0a11b2a6648f920e4bf5a8c191e482bc956a0c22965a0c65cb06a642b" }, "pipfile-spec": 6, "requires": { @@ -72,6 +72,14 @@ "index": "pypi", "version": "==3.0.1" }, + "django-recurrence": { + "hashes": [ + "sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963", + "sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59" + ], + "index": "pypi", + "version": "==1.10.3" + }, "django-widget-tweaks": { "hashes": [ "sha256:9f91ca4217199b7671971d3c1f323a2bec71a0c27dec6260b3c006fa541bc489", @@ -203,7 +211,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "python-ldap": { diff --git a/recmaint/settings/base.py b/recmaint/settings/base.py index 5adfdc8..455268b 100644 --- a/recmaint/settings/base.py +++ b/recmaint/settings/base.py @@ -26,6 +26,7 @@ INSTALLED_APPS = [ 'widget_tweaks', 'markdownx', 'markdownify', + 'recurrence', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/tasks/migrations/0003_task_recurrence.py b/tasks/migrations/0003_task_recurrence.py new file mode 100644 index 0000000..47e776e --- /dev/null +++ b/tasks/migrations/0003_task_recurrence.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.3 on 2021-05-19 21:46 + +from django.db import migrations +import recurrence +import recurrence.fields + +def transfer_recurrence(apps, schema_editor): + Task = apps.get_model('tasks', 'task') + for task in Task.objects.all(): + task.recurrence = recurrence.deserialize('RRULE:' + task.recurrence_interval) + task.save(update_fields=['recurrence']) + + +class Migration(migrations.Migration): + dependencies = [ + ('tasks', '0002_tool_slug'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='recurrence', + field=recurrence.fields.RecurrenceField(default=''), + preserve_default=False, + ), + migrations.RunPython(transfer_recurrence), + migrations.RemoveField( + model_name='task', + name='recurrence_interval', + ), + ] diff --git a/tasks/models.py b/tasks/models.py index eed329d..45c0564 100644 --- a/tasks/models.py +++ b/tasks/models.py @@ -1,11 +1,11 @@ -from datetime import 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 from markdownx.models import MarkdownxField +from recurrence.fields import RecurrenceField class Tool(models.Model): name = models.CharField(max_length=200) @@ -24,7 +24,7 @@ class Task(models.Model): slug = models.SlugField() tool = models.ForeignKey(Tool, on_delete=models.CASCADE) description = MarkdownxField(blank=True) - recurrence_interval = models.CharField(max_length=200) + recurrence = RecurrenceField(include_dtstart=False) recurrence_base = models.DateField(null=True, blank=True) class Meta: @@ -42,18 +42,20 @@ class Task(models.Model): @property def next_recurrence(self): + def _date_to_datetime(date): + return datetime.datetime.combine(date, datetime.time.min) if self.recurrence_base is None: # relative date try: - rrule = rrulestr(self.recurrence_interval, dtstart=self.last_event.date) - return rrule[1] + return self.recurrence.after( + _date_to_datetime(self.last_event.date), + dtstart=_date_to_datetime(self.last_event.date)) except Event.DoesNotExist: return None else: # absolute date - rrule = rrulestr(self.recurrence_interval, dtstart=self.recurrence_base) try: - return rrule.after(self.last_event.date) + return self.recurrence.after(_date_to_datetime(self.last_event.date)) except Event.DoesNotExist: - return rrule[1] + return self.recurrence.occurrences()[0] @property def is_overdue(self): @@ -61,7 +63,7 @@ class Task(models.Model): if next_rec is None: return False else: - return next_rec < datetime.now() + return next_rec < datetime.datetime.now() class SubscriptionSettings(models.Model): @@ -104,7 +106,7 @@ class GroupTaskSubscription(SubscriptionSettings): next_recurrence = self.task.next_recurrence if next_recurrence is None: return False - time_until_overdue = next_recurrence - datetime.now() + time_until_overdue = next_recurrence - datetime.datetime.now() return self.task.is_overdue or (time_until_overdue.days <= self.days_before) def __str__(self): diff --git a/tasks/templates/tasks/taskDetail.djhtml b/tasks/templates/tasks/taskDetail.djhtml index 5c4aae4..7203700 100644 --- a/tasks/templates/tasks/taskDetail.djhtml +++ b/tasks/templates/tasks/taskDetail.djhtml @@ -14,14 +14,26 @@ -

Next scheduled time: {{ task.next_recurrence|date }}

- {% if task.is_overdue %} -
- Task is overdue! -
- {% endif %} +
+

Recurrence

+ - {{ task.description|markdownify }} +

Next scheduled time: {{ task.next_recurrence|date|default:"never" }}

+ {% if task.is_overdue %} +
+ Task is overdue! +
+ {% endif %} +
+ +
+

Description

+ {{ task.description|markdownify }} +
{% if form.errors %}