From 541c7a6a5713a3e3aab7d981a1deba5e689f2301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adeodato=20Sim=C3=B3?= Date: Fri, 20 Oct 2023 07:07:54 -0300 Subject: [PATCH] Shelter stable dates from USE_TZ Some dates (publication dates, author dates) are meant as _literals_. What the user inputs through a `SelectDateWidget` should be preserved as-is. Django's otherwise-excelent support for timezones interferes with it (see Until a better fate of these columns is determined (do we migrate them to a DateField?), and as a stop-gap measure, we can start being faithful to the data by storing them in the Eastern-most timezone. This is particularly important because 1/1/YYYY is a common pattern in publication dates, given #743. --- bookwyrm/models/author.py | 4 +- bookwyrm/models/book.py | 4 +- bookwyrm/models/fields.py | 40 ++++++++++++++++++-- bookwyrm/tests/views/books/test_edit_book.py | 1 - 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index 981e3c0ccc..8ecca17a42 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -32,8 +32,8 @@ class Author(BookDataModel): max_length=255, blank=True, null=True, deduplication_field=True ) # idk probably other keys would be useful here? - born = fields.DateTimeField(blank=True, null=True) - died = fields.DateTimeField(blank=True, null=True) + born = fields.StableDateField(blank=True, null=True) + died = fields.StableDateField(blank=True, null=True) name = fields.CharField(max_length=255) aliases = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 9e05c03af5..7c1db3dd8d 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -135,8 +135,8 @@ class Book(BookDataModel): preview_image = models.ImageField( upload_to="previews/covers/", blank=True, null=True ) - first_published_date = fields.DateTimeField(blank=True, null=True) - published_date = fields.DateTimeField(blank=True, null=True) + first_published_date = fields.StableDateField(blank=True, null=True) + published_date = fields.StableDateField(blank=True, null=True) objects = InheritanceManager() field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"]) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 28effaf9b2..f6c43bf531 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -1,6 +1,6 @@ """ activitypub-aware django model fields """ from dataclasses import MISSING -from datetime import datetime +from datetime import date, datetime, timedelta import re from uuid import uuid4 from urllib.parse import urljoin @@ -11,7 +11,11 @@ from django.contrib.postgres.fields import CICharField as DjangoCICharField from django.core.exceptions import ValidationError from django.db import models -from django.forms import ClearableFileInput, ImageField as DjangoImageField +from django.forms import ( + ClearableFileInput, + DateField as DjangoDateField, + ImageField as DjangoImageField, +) from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.utils.encoding import filepath_to_uri @@ -537,7 +541,6 @@ def field_to_activity(self, value): def field_from_activity(self, value, allow_external_connections=True): missing_fields = datetime(1970, 1, 1) # "2022-10" => "2022-10-01" try: - # TODO(dato): investigate `ignoretz=True` wrt bookwyrm#3028. date_value = dateutil.parser.parse(value, default=missing_fields) try: return timezone.make_aware(date_value) @@ -547,6 +550,37 @@ def field_from_activity(self, value, allow_external_connections=True): return None +class StableDateFormField(DjangoDateField): # should be in forms/fields.py + """converts datetime to date, purposedly ignoring USE_TZ""" + + def prepare_value(self, value): + if isinstance(value, datetime): + return value.date() + return value + + +class StableDateField(DateTimeField): + """a date in a datetime column, forcibly unaffected by USE_TZ""" + + # TODO: extend to PartialStableDate (or SealedDate). + + def formfield(self, **kwargs): + kwargs.setdefault("form_class", StableDateFormField) + return super().formfield(**kwargs) + + def to_python(self, value): + if isinstance(value, date): + tz = timezone.get_fixed_timezone(timedelta(hours=-12)) + naive_dt = datetime(value.year, value.month, value.day) + # Convert to midnight in a timezone that has a stable date + # across the globe. (This is a hotfix while we keep on + # storing stable dates as DateTimeField.) + return timezone.make_aware(naive_dt, tz) + return super().to_python(value) # XXX Just return value? + + # TODO: override field_from_activity(), if necessary? + + class HtmlField(ActivitypubFieldMixin, models.TextField): """a text field for storing html""" diff --git a/bookwyrm/tests/views/books/test_edit_book.py b/bookwyrm/tests/views/books/test_edit_book.py index b1ab2c64b6..8a59749d8e 100644 --- a/bookwyrm/tests/views/books/test_edit_book.py +++ b/bookwyrm/tests/views/books/test_edit_book.py @@ -211,7 +211,6 @@ def test_create_book(self): book = models.Edition.objects.get(title="New Title") self.assertEqual(book.parent_work.title, "New Title") - @expectedFailure # bookwyrm#3028 def test_create_book_published_date(self): """create a book and verify its publication date""" view = views.ConfirmEditBook.as_view()