We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Admin Area: Cannot delete Source that contains chants
When trying to delete a source that contains chants on the source admin page, we get a source DoesNotExist error. The source delete function works fine for sources that do not contain any chants, so I believe this has to do with deleting the associated chants after the source has already been deleted.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `django/cantusdb_project/main_app/signals.py`
Content:
```
1 import operator
2 from functools import reduce
3 
4 from django.contrib.postgres.search import SearchVector
5 from django.db import models
6 from django.db.models import Value
7 from django.db.models.signals import post_save, post_delete
8 from django.dispatch import receiver
9 
10 import re
11 
12 from main_app.models import Chant
13 from main_app.models import Sequence
14 from main_app.models import Feast
15 
16 
17 @receiver(post_save, sender=Chant)
18 def on_chant_save(instance, **kwargs):
19     update_source_chant_count(instance)
20     update_source_melody_count(instance)
21 
22     update_chant_search_vector(instance)
23     update_volpiano_fields(instance)
24 
25 
26 @receiver(post_delete, sender=Chant)
27 def on_chant_delete(instance, **kwargs):
28     update_source_chant_count(instance)
29     update_source_melody_count(instance)
30 
31 
32 @receiver(post_save, sender=Sequence)
33 def on_sequence_save(instance, **kwargs):
34     update_source_chant_count(instance)
35 
36 
37 @receiver(post_delete, sender=Sequence)
38 def on_sequence_delete(instance, **kwargs):
39     update_source_chant_count(instance)
40 
41 
42 @receiver(post_save, sender=Feast)
43 def on_feast_save(instance, **kwargs):
44     update_prefix_field(instance)
45 
46 
47 def update_chant_search_vector(instance):
48     """When saving an instance of Chant, update its search vector field.
49 
50     Called in on_chant_save()
51     """
52     index_components = instance.index_components()
53     pk = instance.pk
54     search_vectors = []
55 
56     for weight, data in index_components.items():
57         search_vectors.append(
58             SearchVector(Value(data, output_field=models.TextField()), weight=weight)
59         )
60     instance.__class__.objects.filter(pk=pk).update(
61         search_vector=reduce(operator.add, search_vectors)
62     )
63 
64 
65 def update_source_chant_count(instance):
66     """When saving or deleting a Chant or Sequence, update its Source's number_of_chants field
67 
68     Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete()
69     """
70 
71     source = instance.source
72     if source is not None:
73         source.number_of_chants = source.chant_set.count() + source.sequence_set.count()
74         source.save()
75 
76 
77 def update_source_melody_count(instance):
78     """When saving or deleting a Chant, update its Source's number_of_melodies field
79 
80     Called in on_chant_save() and on_chant_delete()
81     """
82     source = instance.source
83     if source is not None:
84         source.number_of_melodies = source.chant_set.filter(
85             volpiano__isnull=False
86         ).count()
87         source.save()
88 
89 
90 def update_volpiano_fields(instance):
91     """When saving a Chant, make sure the chant's volpiano_notes and volpiano_intervals are up-to-date
92 
93     Called in on_chant_save()
94     """
95 
96     def generate_volpiano_notes(volpiano):
97         """
98         Populate the ``volpiano_notes`` field of the ``Chant`` model
99 
100         This field is used for melody search
101 
102         Args:
103             volpiano (str): The content of ``chant.volpiano``
104 
105         Returns:
106             str: Volpiano str with non-note chars and duplicate consecutive notes removed
107         """
108         # unwanted_chars are non-note chars, including the clefs, barlines, and accidentals etc.
109         # the `searchMelody.js` on old cantus makes no reference to the b-flat accidentals ("y", "i", "z")
110         # so put them in unwanted chars for now
111         unwanted_chars = [
112             "-",
113             "1",
114             "2",
115             "3",
116             "4",
117             "5",
118             "6",
119             "7",
120             "?",
121             ".",
122             " ",
123             "y",
124             "i",
125             "z",
126         ]
127         # convert all charactors to lower-case, upper-case letters stand for liquescent of the same pitch
128         volpiano_lower = volpiano.lower()
129         # `)` stands for the lowest `g` note liquescent in volpiano, its 'lower case' is `9`
130         volpiano_notes = volpiano_lower.replace(")", "9")
131         # remove none-note charactors
132         for unwanted_char in unwanted_chars:
133             volpiano_notes = volpiano_notes.replace(unwanted_char, "")
134         # remove duplicate consecutive chars
135         volpiano_notes = re.sub(r"(.)\1+", r"\1", volpiano_notes)
136         return volpiano_notes
137 
138     def generate_volpiano_intervals(volpiano_notes):
139         """
140         Populate the ``volpiano_intervals`` field of the ``Chant`` model
141 
142         This field is used for melody search when searching for transpositions
143 
144         Args:
145             volpiano_notes (str): The content of ``chant.volpiano_notes``,
146             populated by the ``generate_volpiano_notes`` function
147 
148         Returns:
149             str: A str of digits, recording the intervals between adjacent notes
150         """
151         # replace '9' (the note G) with the char corresponding to (ASCII(a) - 1), because 'a' denotes the note A
152         volpiano_notes = volpiano_notes.replace("9", chr(ord("a") - 1))
153         # we model the interval between notes using the difference between the ASCII codes of corresponding letters
154         # the letter for the note B is "j" (106), note A is "h" (104), the letter "i" (105) is skipped
155         # move all notes above A down by one letter
156         volpiano_notes = list(volpiano_notes)
157         for j, note in enumerate(volpiano_notes):
158             if ord(note) >= 106:
159                 volpiano_notes[j] = chr(ord(note) - 1)
160 
161         # `intervals` records the difference between two adjacent notes.
162         # Note that intervals are encoded by counting the number of scale
163         # steps between adjacent notes: an ascending second is thus encoded
164         # as "1"; a descending third is encoded "-2", and so on.
165         intervals = []
166         for j in range(1, len(volpiano_notes)):
167             intervals.append(ord(volpiano_notes[j]) - ord(volpiano_notes[j - 1]))
168         # convert `intervals` to str
169         volpiano_intervals = "".join([str(interval) for interval in intervals])
170         return volpiano_intervals
171 
172     if instance.volpiano is None:
173         return
174 
175     volpiano_notes = generate_volpiano_notes(instance.volpiano)
176     volpiano_intervals = generate_volpiano_intervals(volpiano_notes)
177 
178     Chant.objects.filter(id=instance.id).update(
179         volpiano_notes=volpiano_notes,
180         volpiano_intervals=volpiano_intervals,
181     )
182 
183 
184 def update_prefix_field(instance):
185     pk = instance.pk
186 
187     if instance.feast_code:
188         prefix = str(instance.feast_code)[0:2]
189         instance.__class__.objects.filter(pk=pk).update(prefix=prefix)
190     else:  # feast_code is None, ""
191         instance.__class__.objects.filter(pk=pk).update(prefix="")
192
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/django/cantusdb_project/main_app/signals.py b/django/cantusdb_project/main_app/signals.py
--- a/django/cantusdb_project/main_app/signals.py
+++ b/django/cantusdb_project/main_app/signals.py
@@ -12,6 +12,7 @@
 from main_app.models import Chant
 from main_app.models import Sequence
 from main_app.models import Feast
+from main_app.models import Source
 
 
 @receiver(post_save, sender=Chant)
@@ -68,7 +69,11 @@
     Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete()
     """
 
-    source = instance.source
+    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist
+    try:
+        source = instance.source
+    except Source.DoesNotExist:
+        source = None
     if source is not None:
         source.number_of_chants = source.chant_set.count() + source.sequence_set.count()
         source.save()
@@ -79,7 +84,12 @@
 
     Called in on_chant_save() and on_chant_delete()
     """
-    source = instance.source
+
+    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist
+    try:
+        source = instance.source
+    except Source.DoesNotExist:
+        source = None
     if source is not None:
         source.number_of_melodies = source.chant_set.filter(
             volpiano__isnull=False

{"golden_diff": "diff --git a/django/cantusdb_project/main_app/signals.py b/django/cantusdb_project/main_app/signals.py\n--- a/django/cantusdb_project/main_app/signals.py\n+++ b/django/cantusdb_project/main_app/signals.py\n@@ -12,6 +12,7 @@\n from main_app.models import Chant\n from main_app.models import Sequence\n from main_app.models import Feast\n+from main_app.models import Source\n \n \n @receiver(post_save, sender=Chant)\n@@ -68,7 +69,11 @@\n     Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete()\n     \"\"\"\n \n-    source = instance.source\n+    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist\n+    try:\n+        source = instance.source\n+    except Source.DoesNotExist:\n+        source = None\n     if source is not None:\n         source.number_of_chants = source.chant_set.count() + source.sequence_set.count()\n         source.save()\n@@ -79,7 +84,12 @@\n \n     Called in on_chant_save() and on_chant_delete()\n     \"\"\"\n-    source = instance.source\n+\n+    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist\n+    try:\n+        source = instance.source\n+    except Source.DoesNotExist:\n+        source = None\n     if source is not None:\n         source.number_of_melodies = source.chant_set.filter(\n             volpiano__isnull=False\n", "issue": "Admin Area: Cannot delete Source that contains chants\nWhen trying to delete a source that contains chants on the source admin page, we get a source DoesNotExist error. The source delete function works fine for sources that do not contain any chants, so I believe this has to do with deleting the associated chants after the source has already been deleted.\n", "before_files": [{"content": "import operator\nfrom functools import reduce\n\nfrom django.contrib.postgres.search import SearchVector\nfrom django.db import models\nfrom django.db.models import Value\nfrom django.db.models.signals import post_save, post_delete\nfrom django.dispatch import receiver\n\nimport re\n\nfrom main_app.models import Chant\nfrom main_app.models import Sequence\nfrom main_app.models import Feast\n\n\n@receiver(post_save, sender=Chant)\ndef on_chant_save(instance, **kwargs):\n    update_source_chant_count(instance)\n    update_source_melody_count(instance)\n\n    update_chant_search_vector(instance)\n    update_volpiano_fields(instance)\n\n\n@receiver(post_delete, sender=Chant)\ndef on_chant_delete(instance, **kwargs):\n    update_source_chant_count(instance)\n    update_source_melody_count(instance)\n\n\n@receiver(post_save, sender=Sequence)\ndef on_sequence_save(instance, **kwargs):\n    update_source_chant_count(instance)\n\n\n@receiver(post_delete, sender=Sequence)\ndef on_sequence_delete(instance, **kwargs):\n    update_source_chant_count(instance)\n\n\n@receiver(post_save, sender=Feast)\ndef on_feast_save(instance, **kwargs):\n    update_prefix_field(instance)\n\n\ndef update_chant_search_vector(instance):\n    \"\"\"When saving an instance of Chant, update its search vector field.\n\n    Called in on_chant_save()\n    \"\"\"\n    index_components = instance.index_components()\n    pk = instance.pk\n    search_vectors = []\n\n    for weight, data in index_components.items():\n        search_vectors.append(\n            SearchVector(Value(data, output_field=models.TextField()), weight=weight)\n        )\n    instance.__class__.objects.filter(pk=pk).update(\n        search_vector=reduce(operator.add, search_vectors)\n    )\n\n\ndef update_source_chant_count(instance):\n    \"\"\"When saving or deleting a Chant or Sequence, update its Source's number_of_chants field\n\n    Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete()\n    \"\"\"\n\n    source = instance.source\n    if source is not None:\n        source.number_of_chants = source.chant_set.count() + source.sequence_set.count()\n        source.save()\n\n\ndef update_source_melody_count(instance):\n    \"\"\"When saving or deleting a Chant, update its Source's number_of_melodies field\n\n    Called in on_chant_save() and on_chant_delete()\n    \"\"\"\n    source = instance.source\n    if source is not None:\n        source.number_of_melodies = source.chant_set.filter(\n            volpiano__isnull=False\n        ).count()\n        source.save()\n\n\ndef update_volpiano_fields(instance):\n    \"\"\"When saving a Chant, make sure the chant's volpiano_notes and volpiano_intervals are up-to-date\n\n    Called in on_chant_save()\n    \"\"\"\n\n    def generate_volpiano_notes(volpiano):\n        \"\"\"\n        Populate the ``volpiano_notes`` field of the ``Chant`` model\n\n        This field is used for melody search\n\n        Args:\n            volpiano (str): The content of ``chant.volpiano``\n\n        Returns:\n            str: Volpiano str with non-note chars and duplicate consecutive notes removed\n        \"\"\"\n        # unwanted_chars are non-note chars, including the clefs, barlines, and accidentals etc.\n        # the `searchMelody.js` on old cantus makes no reference to the b-flat accidentals (\"y\", \"i\", \"z\")\n        # so put them in unwanted chars for now\n        unwanted_chars = [\n            \"-\",\n            \"1\",\n            \"2\",\n            \"3\",\n            \"4\",\n            \"5\",\n            \"6\",\n            \"7\",\n            \"?\",\n            \".\",\n            \" \",\n            \"y\",\n            \"i\",\n            \"z\",\n        ]\n        # convert all charactors to lower-case, upper-case letters stand for liquescent of the same pitch\n        volpiano_lower = volpiano.lower()\n        # `)` stands for the lowest `g` note liquescent in volpiano, its 'lower case' is `9`\n        volpiano_notes = volpiano_lower.replace(\")\", \"9\")\n        # remove none-note charactors\n        for unwanted_char in unwanted_chars:\n            volpiano_notes = volpiano_notes.replace(unwanted_char, \"\")\n        # remove duplicate consecutive chars\n        volpiano_notes = re.sub(r\"(.)\\1+\", r\"\\1\", volpiano_notes)\n        return volpiano_notes\n\n    def generate_volpiano_intervals(volpiano_notes):\n        \"\"\"\n        Populate the ``volpiano_intervals`` field of the ``Chant`` model\n\n        This field is used for melody search when searching for transpositions\n\n        Args:\n            volpiano_notes (str): The content of ``chant.volpiano_notes``,\n            populated by the ``generate_volpiano_notes`` function\n\n        Returns:\n            str: A str of digits, recording the intervals between adjacent notes\n        \"\"\"\n        # replace '9' (the note G) with the char corresponding to (ASCII(a) - 1), because 'a' denotes the note A\n        volpiano_notes = volpiano_notes.replace(\"9\", chr(ord(\"a\") - 1))\n        # we model the interval between notes using the difference between the ASCII codes of corresponding letters\n        # the letter for the note B is \"j\" (106), note A is \"h\" (104), the letter \"i\" (105) is skipped\n        # move all notes above A down by one letter\n        volpiano_notes = list(volpiano_notes)\n        for j, note in enumerate(volpiano_notes):\n            if ord(note) >= 106:\n                volpiano_notes[j] = chr(ord(note) - 1)\n\n        # `intervals` records the difference between two adjacent notes.\n        # Note that intervals are encoded by counting the number of scale\n        # steps between adjacent notes: an ascending second is thus encoded\n        # as \"1\"; a descending third is encoded \"-2\", and so on.\n        intervals = []\n        for j in range(1, len(volpiano_notes)):\n            intervals.append(ord(volpiano_notes[j]) - ord(volpiano_notes[j - 1]))\n        # convert `intervals` to str\n        volpiano_intervals = \"\".join([str(interval) for interval in intervals])\n        return volpiano_intervals\n\n    if instance.volpiano is None:\n        return\n\n    volpiano_notes = generate_volpiano_notes(instance.volpiano)\n    volpiano_intervals = generate_volpiano_intervals(volpiano_notes)\n\n    Chant.objects.filter(id=instance.id).update(\n        volpiano_notes=volpiano_notes,\n        volpiano_intervals=volpiano_intervals,\n    )\n\n\ndef update_prefix_field(instance):\n    pk = instance.pk\n\n    if instance.feast_code:\n        prefix = str(instance.feast_code)[0:2]\n        instance.__class__.objects.filter(pk=pk).update(prefix=prefix)\n    else:  # feast_code is None, \"\"\n        instance.__class__.objects.filter(pk=pk).update(prefix=\"\")\n", "path": "django/cantusdb_project/main_app/signals.py"}], "after_files": [{"content": "import operator\nfrom functools import reduce\n\nfrom django.contrib.postgres.search import SearchVector\nfrom django.db import models\nfrom django.db.models import Value\nfrom django.db.models.signals import post_save, post_delete\nfrom django.dispatch import receiver\n\nimport re\n\nfrom main_app.models import Chant\nfrom main_app.models import Sequence\nfrom main_app.models import Feast\nfrom main_app.models import Source\n\n\n@receiver(post_save, sender=Chant)\ndef on_chant_save(instance, **kwargs):\n    update_source_chant_count(instance)\n    update_source_melody_count(instance)\n\n    update_chant_search_vector(instance)\n    update_volpiano_fields(instance)\n\n\n@receiver(post_delete, sender=Chant)\ndef on_chant_delete(instance, **kwargs):\n    update_source_chant_count(instance)\n    update_source_melody_count(instance)\n\n\n@receiver(post_save, sender=Sequence)\ndef on_sequence_save(instance, **kwargs):\n    update_source_chant_count(instance)\n\n\n@receiver(post_delete, sender=Sequence)\ndef on_sequence_delete(instance, **kwargs):\n    update_source_chant_count(instance)\n\n\n@receiver(post_save, sender=Feast)\ndef on_feast_save(instance, **kwargs):\n    update_prefix_field(instance)\n\n\ndef update_chant_search_vector(instance):\n    \"\"\"When saving an instance of Chant, update its search vector field.\n\n    Called in on_chant_save()\n    \"\"\"\n    index_components = instance.index_components()\n    pk = instance.pk\n    search_vectors = []\n\n    for weight, data in index_components.items():\n        search_vectors.append(\n            SearchVector(Value(data, output_field=models.TextField()), weight=weight)\n        )\n    instance.__class__.objects.filter(pk=pk).update(\n        search_vector=reduce(operator.add, search_vectors)\n    )\n\n\ndef update_source_chant_count(instance):\n    \"\"\"When saving or deleting a Chant or Sequence, update its Source's number_of_chants field\n\n    Called in on_chant_save(), on_chant_delete(), on_sequence_save() and on_sequence_delete()\n    \"\"\"\n\n    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist\n    try:\n        source = instance.source\n    except Source.DoesNotExist:\n        source = None\n    if source is not None:\n        source.number_of_chants = source.chant_set.count() + source.sequence_set.count()\n        source.save()\n\n\ndef update_source_melody_count(instance):\n    \"\"\"When saving or deleting a Chant, update its Source's number_of_melodies field\n\n    Called in on_chant_save() and on_chant_delete()\n    \"\"\"\n\n    # When a source is deleted (which in turn calls on_chant_delete() on all of its chants) instance.source does not exist\n    try:\n        source = instance.source\n    except Source.DoesNotExist:\n        source = None\n    if source is not None:\n        source.number_of_melodies = source.chant_set.filter(\n            volpiano__isnull=False\n        ).count()\n        source.save()\n\n\ndef update_volpiano_fields(instance):\n    \"\"\"When saving a Chant, make sure the chant's volpiano_notes and volpiano_intervals are up-to-date\n\n    Called in on_chant_save()\n    \"\"\"\n\n    def generate_volpiano_notes(volpiano):\n        \"\"\"\n        Populate the ``volpiano_notes`` field of the ``Chant`` model\n\n        This field is used for melody search\n\n        Args:\n            volpiano (str): The content of ``chant.volpiano``\n\n        Returns:\n            str: Volpiano str with non-note chars and duplicate consecutive notes removed\n        \"\"\"\n        # unwanted_chars are non-note chars, including the clefs, barlines, and accidentals etc.\n        # the `searchMelody.js` on old cantus makes no reference to the b-flat accidentals (\"y\", \"i\", \"z\")\n        # so put them in unwanted chars for now\n        unwanted_chars = [\n            \"-\",\n            \"1\",\n            \"2\",\n            \"3\",\n            \"4\",\n            \"5\",\n            \"6\",\n            \"7\",\n            \"?\",\n            \".\",\n            \" \",\n            \"y\",\n            \"i\",\n            \"z\",\n        ]\n        # convert all charactors to lower-case, upper-case letters stand for liquescent of the same pitch\n        volpiano_lower = volpiano.lower()\n        # `)` stands for the lowest `g` note liquescent in volpiano, its 'lower case' is `9`\n        volpiano_notes = volpiano_lower.replace(\")\", \"9\")\n        # remove none-note charactors\n        for unwanted_char in unwanted_chars:\n            volpiano_notes = volpiano_notes.replace(unwanted_char, \"\")\n        # remove duplicate consecutive chars\n        volpiano_notes = re.sub(r\"(.)\\1+\", r\"\\1\", volpiano_notes)\n        return volpiano_notes\n\n    def generate_volpiano_intervals(volpiano_notes):\n        \"\"\"\n        Populate the ``volpiano_intervals`` field of the ``Chant`` model\n\n        This field is used for melody search when searching for transpositions\n\n        Args:\n            volpiano_notes (str): The content of ``chant.volpiano_notes``,\n            populated by the ``generate_volpiano_notes`` function\n\n        Returns:\n            str: A str of digits, recording the intervals between adjacent notes\n        \"\"\"\n        # replace '9' (the note G) with the char corresponding to (ASCII(a) - 1), because 'a' denotes the note A\n        volpiano_notes = volpiano_notes.replace(\"9\", chr(ord(\"a\") - 1))\n        # we model the interval between notes using the difference between the ASCII codes of corresponding letters\n        # the letter for the note B is \"j\" (106), note A is \"h\" (104), the letter \"i\" (105) is skipped\n        # move all notes above A down by one letter\n        volpiano_notes = list(volpiano_notes)\n        for j, note in enumerate(volpiano_notes):\n            if ord(note) >= 106:\n                volpiano_notes[j] = chr(ord(note) - 1)\n\n        # `intervals` records the difference between two adjacent notes.\n        # Note that intervals are encoded by counting the number of scale\n        # steps between adjacent notes: an ascending second is thus encoded\n        # as \"1\"; a descending third is encoded \"-2\", and so on.\n        intervals = []\n        for j in range(1, len(volpiano_notes)):\n            intervals.append(ord(volpiano_notes[j]) - ord(volpiano_notes[j - 1]))\n        # convert `intervals` to str\n        volpiano_intervals = \"\".join([str(interval) for interval in intervals])\n        return volpiano_intervals\n\n    if instance.volpiano is None:\n        return\n\n    volpiano_notes = generate_volpiano_notes(instance.volpiano)\n    volpiano_intervals = generate_volpiano_intervals(volpiano_notes)\n\n    Chant.objects.filter(id=instance.id).update(\n        volpiano_notes=volpiano_notes,\n        volpiano_intervals=volpiano_intervals,\n    )\n\n\ndef update_prefix_field(instance):\n    pk = instance.pk\n\n    if instance.feast_code:\n        prefix = str(instance.feast_code)[0:2]\n        instance.__class__.objects.filter(pk=pk).update(prefix=prefix)\n    else:  # feast_code is None, \"\"\n        instance.__class__.objects.filter(pk=pk).update(prefix=\"\")\n", "path": "django/cantusdb_project/main_app/signals.py"}]}