Repository: bookwyrm-social/bookwyrm

Issue:
Prevent users from creating readthroughs that end before they start
**Describe the bug**
Right now, a user can set any arbitrary date, past or future for the start and finish dates of a `ReadThrough`. I think future dates are fine, but that it ought to prevent you from setting the finish date as earlier than the start date. 

**Expected behavior**
The form should reject invalid dates and show error text on the page. Ideally it would check the dates before form submission, but it should also (or instead) check in the view_actions and return error text.


Discussion:
hi! could I take this?
yes! go for it, and let me know if you have any questions :)

Resolving patch:
diff --git a/bookwyrm/migrations/0086_auto_20210827_1727.py b/bookwyrm/migrations/0086_auto_20210827_1727.py
new file mode 100644
--- /dev/null
+++ b/bookwyrm/migrations/0086_auto_20210827_1727.py
@@ -0,0 +1,40 @@
+# Generated by Django 3.2.4 on 2021-08-27 17:27
+
+from django.db import migrations, models
+import django.db.models.expressions
+
+
+def normalize_readthrough_dates(app_registry, schema_editor):
+    """Find any invalid dates and reset them"""
+    db_alias = schema_editor.connection.alias
+    app_registry.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter(
+        start_date__gt=models.F("finish_date")
+    ).update(start_date=models.F("finish_date"))
+
+
+def reverse_func(apps, schema_editor):
+    """nothing to do here"""
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookwyrm", "0085_user_saved_lists"),
+    ]
+
+    operations = [
+        migrations.RunPython(normalize_readthrough_dates, reverse_func),
+        migrations.AlterModelOptions(
+            name="readthrough",
+            options={"ordering": ("-start_date",)},
+        ),
+        migrations.AddConstraint(
+            model_name="readthrough",
+            constraint=models.CheckConstraint(
+                check=models.Q(
+                    ("finish_date__gte", django.db.models.expressions.F("start_date"))
+                ),
+                name="chronology",
+            ),
+        ),
+    ]
diff --git a/bookwyrm/migrations/0087_merge_0086_auto_20210827_1727_0086_auto_20210828_1724.py b/bookwyrm/migrations/0087_merge_0086_auto_20210827_1727_0086_auto_20210828_1724.py
new file mode 100644
--- /dev/null
+++ b/bookwyrm/migrations/0087_merge_0086_auto_20210827_1727_0086_auto_20210828_1724.py
@@ -0,0 +1,13 @@
+# Generated by Django 3.2.4 on 2021-08-29 18:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookwyrm", "0086_auto_20210827_1727"),
+        ("bookwyrm", "0086_auto_20210828_1724"),
+    ]
+
+    operations = []
diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py
--- a/bookwyrm/models/readthrough.py
+++ b/bookwyrm/models/readthrough.py
@@ -1,7 +1,8 @@
 """ progress in a book """
+from django.core import validators
 from django.db import models
+from django.db.models import F, Q
 from django.utils import timezone
-from django.core import validators
 
 from .base_model import BookWyrmModel
 
@@ -41,6 +42,16 @@ def create_update(self):
             )
         return None
 
+    class Meta:
+        """Don't let readthroughs end before they start"""
+
+        constraints = [
+            models.CheckConstraint(
+                check=Q(finish_date__gte=F("start_date")), name="chronology"
+            )
+        ]
+        ordering = ("-start_date",)
+
 
 class ProgressUpdate(BookWyrmModel):
     """Store progress through a book in the database."""


Test patch:
diff --git a/bookwyrm/tests/models/test_readthrough_model.py b/bookwyrm/tests/models/test_readthrough_model.py
--- a/bookwyrm/tests/models/test_readthrough_model.py
+++ b/bookwyrm/tests/models/test_readthrough_model.py
@@ -1,7 +1,9 @@
 """ testing models """
+import datetime
 from unittest.mock import patch
 from django.test import TestCase
 from django.core.exceptions import ValidationError
+from django.utils import timezone
 
 from bookwyrm import models
 
@@ -21,27 +23,79 @@ def setUp(self):
             title="Example Edition", parent_work=self.work
         )
 
-        self.readthrough = models.ReadThrough.objects.create(
-            user=self.user, book=self.edition
+    def test_valid_date(self):
+        """can't finish a book before you start it"""
+        start = timezone.now()
+        finish = start + datetime.timedelta(days=1)
+        # just make sure there's no errors
+        models.ReadThrough.objects.create(
+            user=self.user,
+            book=self.edition,
+            start_date=start,
+            finish_date=finish,
+        )
+
+    def test_valid_date_null_start(self):
+        """can't finish a book before you start it"""
+        start = timezone.now()
+        finish = start + datetime.timedelta(days=1)
+        # just make sure there's no errors
+        models.ReadThrough.objects.create(
+            user=self.user,
+            book=self.edition,
+            finish_date=finish,
+        )
+
+    def test_valid_date_null_finish(self):
+        """can't finish a book before you start it"""
+        start = timezone.now()
+        # just make sure there's no errors
+        models.ReadThrough.objects.create(
+            user=self.user,
+            book=self.edition,
+            start_date=start,
+        )
+
+    def test_valid_date_null(self):
+        """can't finish a book before you start it"""
+        # just make sure there's no errors
+        models.ReadThrough.objects.create(
+            user=self.user,
+            book=self.edition,
+        )
+
+    def test_valid_date_same(self):
+        """can't finish a book before you start it"""
+        start = timezone.now()
+        # just make sure there's no errors
+        models.ReadThrough.objects.create(
+            user=self.user,
+            book=self.edition,
+            start_date=start,
+            finish_date=start,
         )
 
     def test_progress_update(self):
         """Test progress updates"""
-        self.readthrough.create_update()  # No-op, no progress yet
-        self.readthrough.progress = 10
-        self.readthrough.create_update()
-        self.readthrough.progress = 20
-        self.readthrough.progress_mode = models.ProgressMode.PERCENT
-        self.readthrough.create_update()
-
-        updates = self.readthrough.progressupdate_set.order_by("created_date").all()
+        readthrough = models.ReadThrough.objects.create(
+            user=self.user, book=self.edition
+        )
+
+        readthrough.create_update()  # No-op, no progress yet
+        readthrough.progress = 10
+        readthrough.create_update()
+        readthrough.progress = 20
+        readthrough.progress_mode = models.ProgressMode.PERCENT
+        readthrough.create_update()
+
+        updates = readthrough.progressupdate_set.order_by("created_date").all()
         self.assertEqual(len(updates), 2)
         self.assertEqual(updates[0].progress, 10)
         self.assertEqual(updates[0].mode, models.ProgressMode.PAGE)
         self.assertEqual(updates[1].progress, 20)
         self.assertEqual(updates[1].mode, models.ProgressMode.PERCENT)
 
-        self.readthrough.progress = -10
-        self.assertRaises(ValidationError, self.readthrough.clean_fields)
-        update = self.readthrough.create_update()
+        readthrough.progress = -10
+        self.assertRaises(ValidationError, readthrough.clean_fields)
+        update = readthrough.create_update()
         self.assertRaises(ValidationError, update.clean_fields)
diff --git a/bookwyrm/tests/views/test_reading.py b/bookwyrm/tests/views/test_reading.py
--- a/bookwyrm/tests/views/test_reading.py
+++ b/bookwyrm/tests/views/test_reading.py
@@ -107,7 +107,7 @@ def test_finish_reading(self, *_):
             {
                 "post-status": True,
                 "privacy": "followers",
-                "finish_date": "2020-01-07",
+                "finish_date": timezone.now().isoformat(),
                 "id": readthrough.id,
             },
         )