Repository: DDMAL/CantusDB

Issue:
"provenance" dropdown (and "office" and "genre") should behave like RISM dropdown 
Discussing #1117 it was noted that on the source-edit page "provenance" has a blank "------" option for when the choices don't apply.
On chant-edit there are also two lists that behave this way (Office and Genre). These could all behave like the RISM dropdown, so the user isn't expecting one interface and getting another. 


Discussion:

Resolving patch:
diff --git a/django/cantusdb_project/main_app/forms.py b/django/cantusdb_project/main_app/forms.py
--- a/django/cantusdb_project/main_app/forms.py
+++ b/django/cantusdb_project/main_app/forms.py
@@ -104,13 +104,14 @@ class Meta:
             "marginalia": TextInputWidget(),
             # folio: defined below (required)
             # c_sequence: defined below (required)
-            # office: defined below (foreignkey)
-            # genre: defined below (foreignkey)
+            "office": autocomplete.ModelSelect2(url="office-autocomplete"),
+            "genre": autocomplete.ModelSelect2(url="genre-autocomplete"),
             "position": TextInputWidget(),
             "cantus_id": TextInputWidget(),
             "feast": autocomplete.ModelSelect2(url="feast-autocomplete"),
             "mode": TextInputWidget(),
             "differentia": TextInputWidget(),
+            "diff_db": autocomplete.ModelSelect2(url="differentia-autocomplete"),
             "finalis": TextInputWidget(),
             "extra": TextInputWidget(),
             "chant_range": VolpianoInputWidget(),
@@ -136,30 +137,6 @@ class Meta:
         help_text="Each folio starts with '1'.",
     )
 
-    # We use NameModelChoiceField here so the dropdown list of office/mass displays the name
-    # instead of [name] + description
-    office = NameModelChoiceField(
-        queryset=Office.objects.all().order_by("name"),
-        required=False,
-    )
-    office.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
-
-    # We use NameModelChoiceField here so the dropdown list of genres displays the name
-    # instead of [name] + description
-    genre = NameModelChoiceField(
-        queryset=Genre.objects.all().order_by("name"),
-        required=False,
-    )
-    genre.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
-
-    diff_db = forms.ModelChoiceField(
-        queryset=Differentia.objects.all().order_by("id"),
-        required=False,
-    )
-    diff_db.widget.attrs.update(
-        {"class": "form-control custom-select custom-select-sm"}
-    )
-
     manuscript_full_text_std_spelling = forms.CharField(
         required=True,
         widget=TextAreaWidget,
@@ -214,6 +191,7 @@ class Meta:
         widgets = {
             "title": TextInputWidget(),
             "siglum": TextInputWidget(),
+            "provenance": autocomplete.ModelSelect2(url="provenance-autocomplete"),
             "provenance_notes": TextInputWidget(),
             "date": TextInputWidget(),
             "cursus": SelectWidget(),
@@ -246,13 +224,6 @@ class Meta:
             ),
         }
 
-    provenance = forms.ModelChoiceField(
-        queryset=Provenance.objects.all().order_by("name"), required=False
-    )
-    provenance.widget.attrs.update(
-        {"class": "form-control custom-select custom-select-sm"}
-    )
-
     TRUE_FALSE_CHOICES_SOURCE = (
         (True, "Full source"),
         (False, "Fragment or Fragmented"),
@@ -310,14 +281,15 @@ class Meta:
             # folio: defined below (required)
             # c_sequence: defined below (required)
             "feast": autocomplete.ModelSelect2(url="feast-autocomplete"),
-            # office: defined below (foreignkey)
-            # genre: defined below (foreignkey)
+            "office": autocomplete.ModelSelect2(url="office-autocomplete"),
+            "genre": autocomplete.ModelSelect2(url="genre-autocomplete"),
             "position": TextInputWidget(),
             "cantus_id": TextInputWidget(),
             "melody_id": TextInputWidget(),
             "mode": TextInputWidget(),
             "finalis": TextInputWidget(),
             "differentia": TextInputWidget(),
+            "diff_db": autocomplete.ModelSelect2(url="differentia-autocomplete"),
             "extra": TextInputWidget(),
             "image_link": TextInputWidget(),
             "indexing_notes": TextAreaWidget(),
@@ -353,30 +325,6 @@ class Meta:
         help_text="Each folio starts with '1'.",
     )
 
-    # We use NameModelChoiceField here so the dropdown list of office/mass displays the name
-    # instead of [name] + description
-    office = NameModelChoiceField(
-        queryset=Office.objects.all().order_by("name"),
-        required=False,
-    )
-    office.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
-
-    # We use NameModelChoiceField here so the dropdown list of genres displays the name
-    # instead of [name] + description
-    genre = NameModelChoiceField(
-        queryset=Genre.objects.all().order_by("name"),
-        required=False,
-    )
-    genre.widget.attrs.update({"class": "form-control custom-select custom-select-sm"})
-
-    diff_db = forms.ModelChoiceField(
-        queryset=Differentia.objects.all().order_by("id"),
-        required=False,
-    )
-    diff_db.widget.attrs.update(
-        {"class": "form-control custom-select custom-select-sm"}
-    )
-
 
 class SourceEditForm(forms.ModelForm):
     class Meta:
@@ -411,6 +359,7 @@ class Meta:
             "title": TextInputWidget(),
             "rism_siglum": autocomplete.ModelSelect2(url="rismsiglum-autocomplete"),
             "siglum": TextInputWidget(),
+            "provenance": autocomplete.ModelSelect2(url="provenance-autocomplete"),
             "provenance_notes": TextInputWidget(),
             "date": TextInputWidget(),
             "summary": TextAreaWidget(),
@@ -442,13 +391,6 @@ class Meta:
             ),
         }
 
-    provenance = forms.ModelChoiceField(
-        queryset=Provenance.objects.all().order_by("name"), required=False
-    )
-    provenance.widget.attrs.update(
-        {"class": "form-control custom-select custom-select-sm"}
-    )  # adds styling
-
     CHOICES_FULL_SOURCE = (
         (None, "None"),
         (True, "Full source"),
diff --git a/django/cantusdb_project/main_app/urls.py b/django/cantusdb_project/main_app/urls.py
--- a/django/cantusdb_project/main_app/urls.py
+++ b/django/cantusdb_project/main_app/urls.py
@@ -68,6 +68,10 @@
     CenturyAutocomplete,
     RismSiglumAutocomplete,
     FeastAutocomplete,
+    OfficeAutocomplete,
+    GenreAutocomplete,
+    DifferentiaAutocomplete,
+    ProvenanceAutocomplete,
     ProofreadByAutocomplete,
 )
 
@@ -472,6 +476,26 @@
         ProofreadByAutocomplete.as_view(),
         name="proofread-by-autocomplete",
     ),
+    path(
+        "provenance-autocomplete/",
+        ProvenanceAutocomplete.as_view(),
+        name="provenance-autocomplete",
+    ),
+    path(
+        "office-autocomplete/",
+        OfficeAutocomplete.as_view(),
+        name="office-autocomplete",
+    ),
+    path(
+        "genre-autocomplete/",
+        GenreAutocomplete.as_view(),
+        name="genre-autocomplete",
+    ),
+    path(
+        "differentia-autocomplete/",
+        DifferentiaAutocomplete.as_view(),
+        name="differentia-autocomplete",
+    ),
 ]
 
 handler404 = "main_app.views.views.handle404"
diff --git a/django/cantusdb_project/main_app/views/views.py b/django/cantusdb_project/main_app/views/views.py
--- a/django/cantusdb_project/main_app/views/views.py
+++ b/django/cantusdb_project/main_app/views/views.py
@@ -8,6 +8,7 @@
 from main_app.models import (
     Century,
     Chant,
+    Differentia,
     Feast,
     Genre,
     Notation,
@@ -1018,6 +1019,56 @@ def get_queryset(self):
         return qs
 
 
+class OfficeAutocomplete(autocomplete.Select2QuerySetView):
+    def get_result_label(self, office):
+        return f"{office.name} - {office.description}"
+
+    def get_queryset(self):
+        if not self.request.user.is_authenticated:
+            return Office.objects.none()
+        qs = Office.objects.all().order_by("name")
+        if self.q:
+            qs = qs.filter(
+                Q(name__istartswith=self.q) | Q(description__istartswith=self.q)
+            )
+        return qs
+
+
+class GenreAutocomplete(autocomplete.Select2QuerySetView):
+    def get_result_label(self, genre):
+        return f"{genre.name} - {genre.description}"
+
+    def get_queryset(self):
+        if not self.request.user.is_authenticated:
+            return Genre.objects.none()
+        qs = Genre.objects.all().order_by("name")
+        if self.q:
+            qs = qs.filter(
+                Q(name__istartswith=self.q) | Q(description__istartswith=self.q)
+            )
+        return qs
+
+
+class DifferentiaAutocomplete(autocomplete.Select2QuerySetView):
+    def get_queryset(self):
+        if not self.request.user.is_authenticated:
+            return Differentia.objects.none()
+        qs = Differentia.objects.all().order_by("differentia_id")
+        if self.q:
+            qs = qs.filter(differentia_id__istartswith=self.q)
+        return qs
+
+
+class ProvenanceAutocomplete(autocomplete.Select2QuerySetView):
+    def get_queryset(self):
+        if not self.request.user.is_authenticated:
+            return Provenance.objects.none()
+        qs = Provenance.objects.all().order_by("name")
+        if self.q:
+            qs = qs.filter(name__istartswith=self.q)
+        return qs
+
+
 class ProofreadByAutocomplete(autocomplete.Select2QuerySetView):
     def get_queryset(self):
         if not self.request.user.is_authenticated:


Test patch: