Repository: bookwyrm-social/bookwyrm

Issue:
Date Picker begins at 1900
**Describe the bug**
On mobile, when editing a book's data, you can't select any date before January 1, 1900. 

**To Reproduce**
Steps to reproduce the behavior:
1. Open a book in Edit mode in Firefox for Android. 
2. Try to edit the Published Date or Originally Published Date, opening the Date Picker. 
3. Click on the year, opening the year selector scrolly thing. 
4. Scroll as far back in time as you can, it will stop at 1900.

**Expected behavior**
The Date Picker should go back further than 1900, or I should be able to manually type the date (which is not possible currently). 

**Instance**
https://tankie.ml/

---

**Smartphone (please complete the following information):**
    - Device: Samsung Galaxy A52 5G
    - OS: Android 12
    - Browser: Firefox
    - Version: 97.2.0
    


Discussion:
Ah how frustrating! What browser are you using?

The date picker isn't part of BookWyrm itself, it's the browser's [built-in input of type `date`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date), so there is a range of behaviors depending on that.

That element obviously isn't implemented well or consistently across browsers, and I'm very open to finding an alternate solution, but it's a bit beyond my front-end coding pay-grade. Any solution also needs to be screen-reader accessible.
I'm using Firefox 97.2.0 for Android. I figured that the browser might be the issue... Would the solution in this Stack Overflow answer help? It's meant to *restrict* dates, but perhaps setting the minimum date to 0 would work? (I have no idea how it would work with BC)

https://stackoverflow.com/a/51903796
Unfortunately, that issue is about a third-party date picker, so it isn't related to the one that's in use currently in BookWyrm

Resolving patch:
diff --git a/bookwyrm/forms/books.py b/bookwyrm/forms/books.py
--- a/bookwyrm/forms/books.py
+++ b/bookwyrm/forms/books.py
@@ -4,6 +4,7 @@
 from bookwyrm import models
 from bookwyrm.models.fields import ClearableFileInputWithWarning
 from .custom_form import CustomForm
+from .widgets import ArrayWidget, SelectDateWidget, Select
 
 
 # pylint: disable=missing-class-docstring
@@ -14,14 +15,6 @@ class Meta:
         help_texts = {f: None for f in fields}
 
 
-class ArrayWidget(forms.widgets.TextInput):
-    # pylint: disable=unused-argument
-    # pylint: disable=no-self-use
-    def value_from_datadict(self, data, files, name):
-        """get all values for this name"""
-        return [i for i in data.getlist(name) if i]
-
-
 class EditionForm(CustomForm):
     class Meta:
         model = models.Edition
@@ -56,16 +49,16 @@ class Meta:
             "publishers": forms.TextInput(
                 attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
             ),
-            "first_published_date": forms.SelectDateWidget(
+            "first_published_date": SelectDateWidget(
                 attrs={"aria-describedby": "desc_first_published_date"}
             ),
-            "published_date": forms.SelectDateWidget(
+            "published_date": SelectDateWidget(
                 attrs={"aria-describedby": "desc_published_date"}
             ),
             "cover": ClearableFileInputWithWarning(
                 attrs={"aria-describedby": "desc_cover"}
             ),
-            "physical_format": forms.Select(
+            "physical_format": Select(
                 attrs={"aria-describedby": "desc_physical_format"}
             ),
             "physical_format_detail": forms.TextInput(
diff --git a/bookwyrm/forms/widgets.py b/bookwyrm/forms/widgets.py
new file mode 100644
--- /dev/null
+++ b/bookwyrm/forms/widgets.py
@@ -0,0 +1,70 @@
+""" using django model forms """
+from django import forms
+
+
+class ArrayWidget(forms.widgets.TextInput):
+    """Inputs for postgres array fields"""
+
+    # pylint: disable=unused-argument
+    # pylint: disable=no-self-use
+    def value_from_datadict(self, data, files, name):
+        """get all values for this name"""
+        return [i for i in data.getlist(name) if i]
+
+
+class Select(forms.Select):
+    """custom template for select widget"""
+
+    template_name = "widgets/select.html"
+
+
+class SelectDateWidget(forms.SelectDateWidget):
+    """
+    A widget that splits date input into two <select> boxes and a numerical year.
+    """
+
+    template_name = "widgets/addon_multiwidget.html"
+    select_widget = Select
+
+    def get_context(self, name, value, attrs):
+        """sets individual widgets"""
+        context = super().get_context(name, value, attrs)
+        date_context = {}
+        year_name = self.year_field % name
+        date_context["year"] = forms.NumberInput().get_context(
+            name=year_name,
+            value=context["widget"]["value"]["year"],
+            attrs={
+                **context["widget"]["attrs"],
+                "id": f"id_{year_name}",
+                "class": "input",
+            },
+        )
+        month_choices = list(self.months.items())
+        if not self.is_required:
+            month_choices.insert(0, self.month_none_value)
+        month_name = self.month_field % name
+        date_context["month"] = self.select_widget(
+            attrs, choices=month_choices
+        ).get_context(
+            name=month_name,
+            value=context["widget"]["value"]["month"],
+            attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
+        )
+        day_choices = [(i, i) for i in range(1, 32)]
+        if not self.is_required:
+            day_choices.insert(0, self.day_none_value)
+        day_name = self.day_field % name
+        date_context["day"] = self.select_widget(
+            attrs,
+            choices=day_choices,
+        ).get_context(
+            name=day_name,
+            value=context["widget"]["value"]["day"],
+            attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
+        )
+        subwidgets = []
+        for field in self._parse_date_fmt():
+            subwidgets.append(date_context[field]["widget"])
+        context["widget"]["subwidgets"] = subwidgets
+        return context
diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py
--- a/bookwyrm/views/books/edit_book.py
+++ b/bookwyrm/views/books/edit_book.py
@@ -1,13 +1,11 @@
 """ the good stuff! the books! """
 from re import sub, findall
-from dateutil.parser import parse as dateparse
 from django.contrib.auth.decorators import login_required, permission_required
 from django.contrib.postgres.search import SearchRank, SearchVector
 from django.db import transaction
 from django.http import HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
-from django.utils.datastructures import MultiValueDictKeyError
 from django.utils.decorators import method_decorator
 from django.views.decorators.http import require_POST
 from django.views import View
@@ -52,7 +50,6 @@ def post(self, request, book_id):
 
         # either of the above cases requires additional confirmation
         if data.get("add_author"):
-            data = copy_form(data)
             return TemplateResponse(request, "book/edit/edit_book.html", data)
 
         remove_authors = request.POST.getlist("remove_authors")
@@ -118,7 +115,6 @@ def post(self, request):
 
         # go to confirm mode
         if not parent_work_id or data.get("add_author"):
-            data = copy_form(data)
             return TemplateResponse(request, "book/edit/edit_book.html", data)
 
         with transaction.atomic():
@@ -139,21 +135,6 @@ def post(self, request):
         return redirect(f"/book/{book.id}")
 
 
-def copy_form(data):
-    """helper to re-create the date fields in the form"""
-    formcopy = data["form"].data.copy()
-    try:
-        formcopy["first_published_date"] = dateparse(formcopy["first_published_date"])
-    except (MultiValueDictKeyError, ValueError):
-        pass
-    try:
-        formcopy["published_date"] = dateparse(formcopy["published_date"])
-    except (MultiValueDictKeyError, ValueError):
-        pass
-    data["form"].data = formcopy
-    return data
-
-
 def add_authors(request, data):
     """helper for adding authors"""
     add_author = [author for author in request.POST.getlist("add_author") if author]


Test patch: