Repository: DDMAL/CantusDB

Issue:
required fields 
On OldCantus, to create a source you need both a manuscript ID and a siglum (fields marked with asterisk) otherwise it won't create the source. 
NewCantus has no asterisks on these fields, and was quite happy to let me make sources with no siglum (though it does tell me to fill out an ID field if I try to submit without it.)

On the chant level, Folio and Sequence seem to be required fields (they are not on OldCantus!) but are not marked as such with asterisks, either. 


Discussion:

Resolving patch:
diff --git a/django/cantusdb_project/main_app/models/source.py b/django/cantusdb_project/main_app/models/source.py
--- a/django/cantusdb_project/main_app/models/source.py
+++ b/django/cantusdb_project/main_app/models/source.py
@@ -33,8 +33,8 @@ class Source(BaseModel):
     # it is a human-readable ID for a source
     siglum = models.CharField(
         max_length=63, 
-        null=True, 
-        blank=True,
+        null=False, 
+        blank=False,
         help_text="RISM-style siglum + Shelf-mark (e.g. GB-Ob 202).",
     )
     # the RISM siglum uniquely identifies a library or holding institution


Test patch:
diff --git a/django/cantusdb_project/main_app/tests/make_fakes.py b/django/cantusdb_project/main_app/tests/make_fakes.py
--- a/django/cantusdb_project/main_app/tests/make_fakes.py
+++ b/django/cantusdb_project/main_app/tests/make_fakes.py
@@ -260,13 +260,29 @@ def make_fake_sequence(source=None, title=None, incipit=None, cantus_id=None) ->
     return sequence
 
 
-def make_fake_source(published=True, title=None, segment_name=None) -> Source:
+def make_fake_source(published=None, title=None, segment=None, segment_name=None, siglum=None, description=None, summary=None, provenance=None, full_source=None, rism_siglum=None, indexing_notes=None) -> Source:
     """Generates a fake Source object."""
     # The cursus_choices and source_status_choices lists in Source are lists of
     # tuples and we only need the first element of each tuple
 
     if title is None:
-        title=make_fake_text(LONG_CHAR_FIELD_MAX)
+        title = make_fake_text(LONG_CHAR_FIELD_MAX)
+    if siglum is None:
+        siglum = make_fake_text(SHORT_CHAR_FIELD_MAX)
+    if description is None:
+        description = make_fake_text(
+            max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
+        )
+    if published is None:
+        published = True
+    if segment is None:
+        segment = make_fake_segment(name=segment_name)
+    if rism_siglum is None:
+        rism_siglum = make_fake_rism_siglum()
+    if summary is None:
+        summary = make_fake_text(
+            max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
+        )
 
     cursus_choices = [x[0] for x in Source.cursus_choices]
     source_status_choices = [x[0] for x in Source.source_status_choices]
@@ -274,34 +290,28 @@ def make_fake_source(published=True, title=None, segment_name=None) -> Source:
     source = Source.objects.create(
         published=published,
         title=title,
-        siglum=make_fake_text(SHORT_CHAR_FIELD_MAX),
-        rism_siglum=make_fake_rism_siglum(),
-        provenance=make_fake_provenance(),
+        siglum=siglum,
+        rism_siglum=rism_siglum,
+        provenance=provenance,
         provenance_notes=make_fake_text(
             max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
         ),
-        full_source=faker.boolean(),
+        full_source=full_source,
         date=make_fake_text(SHORT_CHAR_FIELD_MAX),
         cursus=random.choice(cursus_choices),
-        segment=make_fake_segment(name=segment_name),
+        segment=segment,
         source_status=random.choice(source_status_choices),
         complete_inventory=faker.boolean(),
-        summary=make_fake_text(
-            max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
-        ),
+        summary=summary,
         liturgical_occasions=make_fake_text(
             max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
         ),
-        description=make_fake_text(
-            max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
-        ),
+        description=description,
         selected_bibliography=make_fake_text(
             max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
         ),
         image_link=faker.image_url(),
-        indexing_notes=make_fake_text(
-            max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
-        ),
+        indexing_notes=indexing_notes,
         indexing_date=make_fake_text(
             max_size=MAX_NUMBER_TEXT_CHARS, min_size=MIN_NUMBER_TEXT_CHARS
         ),
@@ -314,4 +324,5 @@ def make_fake_source(published=True, title=None, segment_name=None) -> Source:
     source.melodies_entered_by.set([make_fake_user()])
     source.proofreaders.set([make_fake_user()])
     source.other_editors.set([make_fake_user()])
+
     return source
diff --git a/django/cantusdb_project/main_app/tests/test_views.py b/django/cantusdb_project/main_app/tests/test_views.py
--- a/django/cantusdb_project/main_app/tests/test_views.py
+++ b/django/cantusdb_project/main_app/tests/test_views.py
@@ -664,7 +664,7 @@ def test_published_vs_unpublished(self):
         self.assertNotIn(chant, response.context["chants"])
 
     def test_search_by_office(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         office = make_fake_office()
         chant = Chant.objects.create(source=source, office=office)
         search_term = get_random_search_term(office.name)
@@ -672,28 +672,28 @@ def test_search_by_office(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_filter_by_genre(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         genre = make_fake_genre()
         chant = Chant.objects.create(source=source, genre=genre)
         response = self.client.get(reverse("chant-search"), {"genre": genre.id})
         self.assertIn(chant, response.context["chants"])
 
     def test_search_by_cantus_id(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         chant = Chant.objects.create(source=source, cantus_id=faker.numerify("######"))
         search_term = get_random_search_term(chant.cantus_id)
         response = self.client.get(reverse("chant-search"), {"cantus_id": search_term})
         self.assertIn(chant, response.context["chants"])
 
     def test_search_by_mode(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         chant = Chant.objects.create(source=source, mode=faker.numerify("#"))
         search_term = get_random_search_term(chant.mode)
         response = self.client.get(reverse("chant-search"), {"mode": search_term})
         self.assertIn(chant, response.context["chants"])
 
     def test_search_by_feast(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         feast = make_fake_feast()
         chant = Chant.objects.create(source=source, feast=feast)
         search_term = get_random_search_term(feast.name)
@@ -701,7 +701,7 @@ def test_search_by_feast(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_search_by_position(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         position = 1
         chant = Chant.objects.create(source=source, position=position)
         search_term = "1"
@@ -709,7 +709,7 @@ def test_search_by_position(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_filter_by_melody(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         chant_with_melody = Chant.objects.create(
             source=source, volpiano=make_fake_text(max_size=20)
         )
@@ -720,7 +720,7 @@ def test_filter_by_melody(self):
         self.assertNotIn(chant_without_melody, response.context["chants"])
 
     def test_keyword_search_starts_with(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         chant = Chant.objects.create(
             source=source, incipit=make_fake_text(max_size=200)
         )
@@ -732,7 +732,7 @@ def test_keyword_search_starts_with(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_keyword_search_contains(self):
-        source = Source.objects.create(published=True, title="a source")
+        source = make_fake_source(published=True)
         chant = Chant.objects.create(
             source=source, manuscript_full_text=make_fake_text(max_size=400)
         )
@@ -772,7 +772,6 @@ def test_published_vs_unpublished(self):
         self.assertEqual(response.status_code, 403)
 
     def test_search_by_office(self):
-        # source = Source.objects.create(public=True,published, title="a source")
         source = make_fake_source()
         office = make_fake_office()
         chant = Chant.objects.create(source=source, office=office)
@@ -893,7 +892,7 @@ def test_chant_source_queryset(self):
         self.assertIn(chant, response.context["chants"])
 
     def test_sequence_source_queryset(self):
-        seq_source = Source.objects.create(
+        seq_source = make_fake_source(
             segment=Segment.objects.create(id=4064, name="Clavis Sequentiarium"),
             title="a sequence source",
             published=True
@@ -1514,7 +1513,7 @@ def test_context(self):
         self.assertEqual(feast, response.context["feast"])
 
     def test_most_frequent_chants(self):
-        source = Source.objects.create(published=True, title="published_source")
+        source = make_fake_source(published=True, title="published_source")
         feast = make_fake_feast()
         # 3 chants with cantus id: 300000
         for i in range(3):
@@ -1557,10 +1556,10 @@ def test_chants_in_feast_published_vs_unpublished(self):
         self.assertNotIn(chant.cantus_id, cantus_ids)
         
     def test_sources_containing_this_feast(self):
-        big_source = Source.objects.create(
+        big_source = make_fake_source(
             published=True, title="big_source", siglum="big"
         )
-        small_source = Source.objects.create(
+        small_source = make_fake_source(
             published=True, title="small_source", siglum="small"
         )
         feast = make_fake_feast()
@@ -1913,7 +1912,7 @@ def test_ordering(self):
 
     def test_search_incipit(self):
         # create a published sequence source and some sequence in it
-        source = Source.objects.create(
+        source = make_fake_source(
             published=True, title="a sequence source"
         )
         sequence = Sequence.objects.create(
@@ -1927,7 +1926,7 @@ def test_search_incipit(self):
 
     def test_search_siglum(self):
         # create a published sequence source and some sequence in it
-        source = Source.objects.create(
+        source = make_fake_source(
             published=True, title="a sequence source"
         )
         sequence = Sequence.objects.create(
@@ -1941,7 +1940,7 @@ def test_search_siglum(self):
 
     def test_search_cantus_id(self):
         # create a published sequence source and some sequence in it
-        source = Source.objects.create(
+        source = make_fake_source(
             published=True, title="a sequence source"
         )
         # faker generates a fake cantus id, in the form of two letters followed by five digits
@@ -2037,10 +2036,10 @@ def test_provenances_and_centuries_in_context(self):
 
     def test_only_published_sources_visible(self):
         """For a source to be displayed in the list, its `published` field must be `True`"""
-        published_source = Source.objects.create(
+        published_source = make_fake_source(
             published=True, title="published source"
         )
-        private_source = Source.objects.create(
+        private_source = make_fake_source(
             published=False, title="private source"
         )
         response = self.client.get(reverse("source-list"))
@@ -2052,10 +2051,10 @@ def test_filter_by_segment(self):
         """The source list can be filtered by `segment`, `provenance`, `century`, and `full_source`"""
         cantus_segment = make_fake_segment(name="cantus")
         clavis_segment = make_fake_segment(name="clavis")
-        chant_source = Source.objects.create(
+        chant_source = make_fake_source(
             segment=cantus_segment, title="chant source", published=True
         )
-        seq_source = Source.objects.create(
+        seq_source = make_fake_source(
             segment=clavis_segment, title="sequence source", published=True
         )
 
@@ -2078,18 +2077,20 @@ def test_filter_by_segment(self):
     def test_filter_by_provenance(self):
         aachen = make_fake_provenance()
         albi = make_fake_provenance()
-        aachen_source = Source.objects.create(
+        aachen_source = make_fake_source(
             provenance=aachen,
             published=True,
             title="source originated in Aachen",
         )
-        albi_source = Source.objects.create(
+        albi_source = make_fake_source(
             provenance=albi,
             published=True,
             title="source originated in Albi",
         )
-        no_provenance_source = Source.objects.create(
-            published=True, title="source with empty provenance"
+        no_provenance_source = make_fake_source(
+            published=True,
+            provenance=None,
+            title="source with empty provenance",
         )
 
         # display sources in Aachen
@@ -2107,17 +2108,17 @@ def test_filter_by_century(self):
         )
         tenth_century = Century.objects.create(name="10th century")
 
-        ninth_century_source = Source.objects.create(
+        ninth_century_source = make_fake_source(
             published=True, title="source",
         )
         ninth_century_source.century.set([ninth_century])
 
-        ninth_century_first_half_source = Source.objects.create(
+        ninth_century_first_half_source = make_fake_source(
             published=True, title="source",
         )
         ninth_century_first_half_source.century.set([ninth_century_first_half])
 
-        multiple_century_source = Source.objects.create(
+        multiple_century_source = make_fake_source(
             published=True, title="source",
         )
         multiple_century_source.century.set([ninth_century, tenth_century])
@@ -2144,14 +2145,20 @@ def test_filter_by_century(self):
         self.assertNotIn(multiple_century_source, sources)
 
     def test_filter_by_full_source(self):
-        full_source = Source.objects.create(
-            full_source=True, published=True, title="full source"
+        full_source = make_fake_source(
+            full_source=True,
+            published=True,
+            title="full source",
         )
-        fragment = Source.objects.create(
-            full_source=False, published=True, title="fragment"
+        fragment = make_fake_source(
+            full_source=False,
+            published=True,
+            title="fragment",
         )
-        unknown = Source.objects.create(
-            published=True, title="full_source field is empty"
+        unknown = make_fake_source(
+            full_source = None,
+            published=True,
+            title="full_source field is empty",
         )
 
         # display full sources
@@ -2180,7 +2187,7 @@ def test_filter_by_full_source(self):
 
     def test_search_by_title(self):
         """The "general search" field searches in `title`, `siglum`, `rism_siglum`, `description`, and `summary`"""
-        source = Source.objects.create(
+        source = make_fake_source(
             title=make_fake_text(max_size=20), published=True
         )
         search_term = get_random_search_term(source.title)
@@ -2188,7 +2195,7 @@ def test_search_by_title(self):
         self.assertIn(source, response.context["sources"])
 
     def test_search_by_siglum(self):
-        source = Source.objects.create(
+        source = make_fake_source(
             siglum=make_fake_text(max_size=20), published=True, title="title"
         )
         search_term = get_random_search_term(source.siglum)
@@ -2197,7 +2204,7 @@ def test_search_by_siglum(self):
 
     def test_search_by_rism_siglum_name(self):
         rism_siglum = make_fake_rism_siglum()
-        source = Source.objects.create(
+        source = make_fake_source(
             rism_siglum=rism_siglum, published=True, title="title",
         )
         search_term = get_random_search_term(source.rism_siglum.name)
@@ -2206,7 +2213,7 @@ def test_search_by_rism_siglum_name(self):
 
     def test_search_by_rism_siglum_description(self):
         rism_siglum = make_fake_rism_siglum()
-        source = Source.objects.create(
+        source = make_fake_source(
             rism_siglum=rism_siglum, published=True, title="title",
         )
         search_term = get_random_search_term(source.rism_siglum.description)
@@ -2214,7 +2221,7 @@ def test_search_by_rism_siglum_description(self):
         self.assertIn(source, response.context["sources"])
 
     def test_search_by_description(self):
-        source = Source.objects.create(
+        source = make_fake_source(
             description=make_fake_text(max_size=200),
             published=True,
             title="title",
@@ -2224,7 +2231,7 @@ def test_search_by_description(self):
         self.assertIn(source, response.context["sources"])
 
     def test_search_by_summary(self):
-        source = Source.objects.create(
+        source = make_fake_source(
             summary=make_fake_text(max_size=200),
             published=True,
             title="title",
@@ -2235,7 +2242,7 @@ def test_search_by_summary(self):
 
     def test_search_by_indexing_notes(self):
         """The "indexing notes" field searches in `indexing_notes` and indexer/editor related fields"""
-        source = Source.objects.create(
+        source = make_fake_source(
             indexing_notes=make_fake_text(max_size=200),
             published=True,
             title="title",
@@ -2271,7 +2278,11 @@ def test_url_and_templates(self):
     def test_create_source(self):
         response = self.client.post(
             reverse('source-create'), 
-            {'title': 'test'})
+            {
+                'title': 'test',
+                'siglum': 'test-siglum' # siglum is a required field
+            }
+        )
 
         self.assertEqual(response.status_code, 302)
         self.assertRedirects(response, reverse('source-create'))       
@@ -2314,8 +2325,12 @@ def test_edit_source(self):
         source = make_fake_source()
 
         response = self.client.post(
-            reverse('source-edit', args=[source.id]), 
-            {'title': 'test'})
+            reverse('source-edit', args=[source.id]),
+            {
+                'title': 'test',
+                'siglum' : 'test-siglum', # siglum is a required field,
+            },
+        )
 
         self.assertEqual(response.status_code, 302)
         self.assertRedirects(response, reverse('source-detail', args=[source.id]))       
@@ -2348,10 +2363,11 @@ def test_context_chant_folios(self):
 
     def test_context_sequence_folios(self):
         # create a sequence source and several sequences in it
-        source = Source.objects.create(
-            segment=Segment.objects.create(id=4064, name="Bower Sequence Database"),
+        bower_segment = Segment.objects.create(id=4064, name="Bower Sequence Database")
+        source = make_fake_source(
             title="a sequence source",
-            published=True
+            published=True,
+            segment=bower_segment
         )
         Sequence.objects.create(source=source, folio="001r")
         Sequence.objects.create(source=source, folio="001r")
@@ -2387,7 +2403,7 @@ def test_context_feasts_with_folios(self):
 
     def test_context_sequences(self):
         # create a sequence source and several sequences in it
-        source = Source.objects.create(
+        source = make_fake_source(
             segment=Segment.objects.create(id=4064, name="Bower Sequence Database"),
             title="a sequence source",
             published=True