We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Source Detail page should not display link to "Manage source editors" to all logged-in users
Currently, in the Source Detail view, if a user is logged in, we display "add new chant", "full text & volpiano editor", "edit source description" and "manage source editors" links in the sidebar. We need to check that the user has permission to edit the source before displaying these links, otherwise, users will click through into 403 errors. An extra check will be required for the "manage source editors" link - only people with "staff status" should see this link, as it leads to the admin area.

--- 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/views/source.py`
Content:
```
1 from django.views.generic import DetailView, ListView, CreateView, UpdateView
2 from django.db.models import Q, Prefetch
3 from main_app.models import Source, Provenance, Century
4 from main_app.forms import SourceCreateForm, SourceEditForm
5 from django.contrib import messages
6 from django.urls import reverse
7 from django.contrib.auth.mixins import LoginRequiredMixin
8 from django.http import HttpResponseRedirect
9 from django.contrib.auth.mixins import UserPassesTestMixin
10 from django.core.exceptions import PermissionDenied
11 from django.shortcuts import get_object_or_404
12 from main_app.views.chant import get_feast_selector_options
13 
14 
15 class SourceDetailView(DetailView):
16     model = Source
17     context_object_name = "source"
18     template_name = "source_detail.html"
19 
20     def get_context_data(self, **kwargs):
21         source = self.get_object()
22         display_unpublished = self.request.user.is_authenticated
23         if (source.published is False) and (not display_unpublished):
24             raise PermissionDenied()
25 
26         context = super().get_context_data(**kwargs)
27 
28         if source.segment and source.segment.id == 4064:
29             # if this is a sequence source
30             context["sequences"] = source.sequence_set.order_by("s_sequence")
31             context["folios"] = (
32                 source.sequence_set.values_list("folio", flat=True)
33                 .distinct()
34                 .order_by("folio")
35             )
36         else:
37             # if this is a chant source
38             folios = (
39                 source.chant_set.values_list("folio", flat=True)
40                 .distinct()
41                 .order_by("folio")
42             )
43             context["folios"] = folios
44             # the options for the feast selector on the right, only chant sources have this
45             context["feasts_with_folios"] = get_feast_selector_options(source, folios)
46         return context
47 
48 
49 class SourceListView(ListView):
50     paginate_by = 100
51     context_object_name = "sources"
52     template_name = "source_list.html"
53 
54     def get_context_data(self, **kwargs):
55         context = super().get_context_data(**kwargs)
56         context["provenances"] = (
57             Provenance.objects.all().order_by("name").values("id", "name")
58         )
59         context["centuries"] = (
60             Century.objects.all().order_by("name").values("id", "name")
61         )
62         return context
63 
64     def get_queryset(self):
65         # use select_related() for foreign keys to reduce DB queries
66         queryset = Source.objects.select_related(
67             "rism_siglum", "segment", "provenance"
68         ).order_by("siglum")
69 
70         display_unpublished = self.request.user.is_authenticated
71         if display_unpublished:
72             q_obj_filter = Q()
73         else:
74             q_obj_filter = Q(published=True)
75 
76         if self.request.GET.get("century"):
77             century_name = Century.objects.get(id=self.request.GET.get("century")).name
78             q_obj_filter &= Q(century__name__icontains=century_name)
79 
80         if self.request.GET.get("provenance"):
81             provenance_id = int(self.request.GET.get("provenance"))
82             q_obj_filter &= Q(provenance__id=provenance_id)
83         if self.request.GET.get("segment"):
84             segment_id = int(self.request.GET.get("segment"))
85             q_obj_filter &= Q(segment__id=segment_id)
86         if self.request.GET.get("fullSource") in ["true", "false"]:
87             full_source_str = self.request.GET.get("fullSource")
88             if full_source_str == "true":
89                 full_source_q = Q(full_source=True) | Q(full_source=None)
90                 q_obj_filter &= full_source_q
91             else:
92                 q_obj_filter &= Q(full_source=False)
93 
94         if self.request.GET.get("general"):
95             # Strip spaces at the beginning and end. Then make list of terms split on spaces
96             general_search_terms = self.request.GET.get("general").strip(" ").split(" ")
97             # We need a Q Object for each field we're gonna look into
98             title_q = Q()
99             siglum_q = Q()
100             rism_siglum_q = Q()
101             description_q = Q()
102             # it seems that old cantus don't look into title and provenance for the general search terms
103             # cantus.uwaterloo.ca/source/123901 this source cannot be found by searching its provenance 'Kremsmünster' in the general search field
104             # provenance_q = Q()
105             summary_q = Q()
106 
107             # For each term, add it to the Q object of each field with an OR operation.
108             # We split the terms so that the words can be separated in the actual
109             # field, allowing for a more flexible search, and a field needs
110             # to match only one of the terms
111             for term in general_search_terms:
112                 title_q |= Q(title__icontains=term)
113                 siglum_q |= Q(siglum__icontains=term)
114                 rism_siglum_q |= Q(rism_siglum__name__icontains=term) | Q(
115                     rism_siglum__description__icontains=term
116                 )
117                 description_q |= Q(description__icontains=term)
118                 summary_q |= Q(summary__icontains=term)
119                 # provenance_q |= Q(provenance__name__icontains=term)
120             # All the Q objects are put together with OR.
121             # The end result is that at least one term has to match in at least one
122             # field
123             # general_search_q = (
124             #     title_q | siglum_q | rism_siglum_q | description_q | provenance_q
125             # )
126             general_search_q = (
127                 title_q | siglum_q | rism_siglum_q | description_q | summary_q
128             )
129             q_obj_filter &= general_search_q
130 
131         # For the indexing notes search we follow the same procedure as above but with
132         # different fields
133         if self.request.GET.get("indexing"):
134             # Make list of terms split on spaces
135             indexing_search_terms = self.request.GET.get("indexing").split(" ")
136             # We need a Q Object for each field we're gonna look into
137             inventoried_by_q = Q()
138             full_text_entered_by_q = Q()
139             melodies_entered_by_q = Q()
140             proofreaders_q = Q()
141             other_editors_q = Q()
142             indexing_notes_q = Q()
143             # For each term, add it to the Q object of each field with an OR operation.
144             # We split the terms so that the words can be separated in the actual
145             # field, allowing for a more flexible search, and a field needs
146             # to match only one of the terms
147             for term in indexing_search_terms:
148                 inventoried_by_q |= Q(inventoried_by__full_name__icontains=term)
149                 full_text_entered_by_q |= Q(
150                     full_text_entered_by__full_name__icontains=term
151                 )
152                 melodies_entered_by_q |= Q(
153                     melodies_entered_by__full_name__icontains=term
154                 )
155                 proofreaders_q |= Q(proofreaders__full_name__icontains=term)
156                 other_editors_q |= Q(other_editors__full_name__icontains=term)
157                 indexing_notes_q |= Q(indexing_notes__icontains=term)
158             # All the Q objects are put together with OR.
159             # The end result is that at least one term has to match in at least one
160             # field
161             indexing_search_q = (
162                 inventoried_by_q
163                 | full_text_entered_by_q
164                 | melodies_entered_by_q
165                 | proofreaders_q
166                 | other_editors_q
167                 | indexing_notes_q
168             )
169             q_obj_filter &= indexing_search_q
170 
171         return queryset.filter(q_obj_filter).prefetch_related(
172             Prefetch("century", queryset=Century.objects.all().order_by("id"))
173         )
174 
175 
176 class SourceCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
177     model = Source
178     template_name = "source_create_form.html"
179     form_class = SourceCreateForm
180 
181     def test_func(self):
182         user = self.request.user
183         # checks if the user is allowed to create sources
184         is_authorized = user.groups.filter(
185             Q(name="project manager") | Q(name="editor") | Q(name="contributor")
186         ).exists()
187 
188         if is_authorized:
189             return True
190         else:
191             return False
192 
193     def get_success_url(self):
194         return reverse("source-detail", args=[self.object.id])
195 
196     def form_valid(self, form):
197         form.instance.created_by = self.request.user
198         self.object = form.save()
199 
200         # assign this source to the "current_editors"
201         current_editors = self.object.current_editors.all()
202         self.request.user.sources_user_can_edit.add(self.object)
203 
204         for editor in current_editors:
205             editor.sources_user_can_edit.add(self.object)
206 
207         messages.success(
208             self.request,
209             "Source created successfully!",
210         )
211         return HttpResponseRedirect(self.get_success_url())
212 
213 
214 class SourceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
215     template_name = "source_edit.html"
216     model = Source
217     form_class = SourceEditForm
218     pk_url_kwarg = "source_id"
219 
220     def get_context_data(self, **kwargs):
221         source = self.get_object()
222         context = super().get_context_data(**kwargs)
223 
224         if source.segment and source.segment.id == 4064:
225             # if this is a sequence source
226             context["sequences"] = source.sequence_set.order_by("s_sequence")
227             context["folios"] = (
228                 source.sequence_set.values_list("folio", flat=True)
229                 .distinct()
230                 .order_by("folio")
231             )
232         else:
233             # if this is a chant source
234             folios = (
235                 source.chant_set.values_list("folio", flat=True)
236                 .distinct()
237                 .order_by("folio")
238             )
239             context["folios"] = folios
240             # the options for the feast selector on the right, only chant sources have this
241             context["feasts_with_folios"] = get_feast_selector_options(source, folios)
242         return context
243 
244     def test_func(self):
245         user = self.request.user
246         source_id = self.kwargs.get(self.pk_url_kwarg)
247         source = get_object_or_404(Source, id=source_id)
248 
249         assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
250 
251         # checks if the user is a project manager
252         is_project_manager = user.groups.filter(name="project manager").exists()
253         # checks if the user is an editor
254         is_editor = user.groups.filter(name="editor").exists()
255         # checks if the user is a contributor
256         is_contributor = user.groups.filter(name="contributor").exists()
257 
258         if (
259             (is_project_manager)
260             or (is_editor and assigned_to_source)
261             or (is_editor and source.created_by == user)
262             or (is_contributor and source.created_by == user)
263         ):
264             return True
265         else:
266             return False
267 
268     def form_valid(self, form):
269         form.instance.last_updated_by = self.request.user
270 
271         # remove this source from the old "current_editors"
272         # assign this source to the new "current_editors"
273 
274         old_current_editors = list(
275             Source.objects.get(id=form.instance.id).current_editors.all()
276         )
277         new_current_editors = form.cleaned_data["current_editors"]
278         source = form.save()
279 
280         for old_editor in old_current_editors:
281             old_editor.sources_user_can_edit.remove(source)
282 
283         for new_editor in new_current_editors:
284             new_editor.sources_user_can_edit.add(source)
285 
286         return HttpResponseRedirect(self.get_success_url())
287
```
--- 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/views/source.py b/django/cantusdb_project/main_app/views/source.py
--- a/django/cantusdb_project/main_app/views/source.py
+++ b/django/cantusdb_project/main_app/views/source.py
@@ -9,7 +9,10 @@
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.exceptions import PermissionDenied
 from django.shortcuts import get_object_or_404
-from main_app.views.chant import get_feast_selector_options
+from main_app.views.chant import (
+    get_feast_selector_options,
+    user_can_edit_chants_in_source,
+)
 
 
 class SourceDetailView(DetailView):
@@ -19,6 +22,7 @@
 
     def get_context_data(self, **kwargs):
         source = self.get_object()
+        user = self.request.user
         display_unpublished = self.request.user.is_authenticated
         if (source.published is False) and (not display_unpublished):
             raise PermissionDenied()
@@ -43,6 +47,9 @@
             context["folios"] = folios
             # the options for the feast selector on the right, only chant sources have this
             context["feasts_with_folios"] = get_feast_selector_options(source, folios)
+
+        context["user_can_edit_chants"] = user_can_edit_chants_in_source(user, source)
+        context["user_can_edit_source"] = user_can_edit_source(user, source)
         return context
 
 
@@ -246,24 +253,7 @@
         source_id = self.kwargs.get(self.pk_url_kwarg)
         source = get_object_or_404(Source, id=source_id)
 
-        assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
-
-        # checks if the user is a project manager
-        is_project_manager = user.groups.filter(name="project manager").exists()
-        # checks if the user is an editor
-        is_editor = user.groups.filter(name="editor").exists()
-        # checks if the user is a contributor
-        is_contributor = user.groups.filter(name="contributor").exists()
-
-        if (
-            (is_project_manager)
-            or (is_editor and assigned_to_source)
-            or (is_editor and source.created_by == user)
-            or (is_contributor and source.created_by == user)
-        ):
-            return True
-        else:
-            return False
+        return user_can_edit_source(user, source)
 
     def form_valid(self, form):
         form.instance.last_updated_by = self.request.user
@@ -284,3 +274,25 @@
             new_editor.sources_user_can_edit.add(source)
 
         return HttpResponseRedirect(self.get_success_url())
+
+
+def user_can_edit_source(user, source):
+    source_id = source.id
+    assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
+
+    # checks if the user is a project manager
+    is_project_manager = user.groups.filter(name="project manager").exists()
+    # checks if the user is an editor
+    is_editor = user.groups.filter(name="editor").exists()
+    # checks if the user is a contributor
+    is_contributor = user.groups.filter(name="contributor").exists()
+
+    if (
+        (is_project_manager)
+        or (is_editor and assigned_to_source)
+        or (is_editor and source.created_by == user)
+        or (is_contributor and source.created_by == user)
+    ):
+        return True
+    else:
+        return False

{"golden_diff": "diff --git a/django/cantusdb_project/main_app/views/source.py b/django/cantusdb_project/main_app/views/source.py\n--- a/django/cantusdb_project/main_app/views/source.py\n+++ b/django/cantusdb_project/main_app/views/source.py\n@@ -9,7 +9,10 @@\n from django.contrib.auth.mixins import UserPassesTestMixin\n from django.core.exceptions import PermissionDenied\n from django.shortcuts import get_object_or_404\n-from main_app.views.chant import get_feast_selector_options\n+from main_app.views.chant import (\n+    get_feast_selector_options,\n+    user_can_edit_chants_in_source,\n+)\n \n \n class SourceDetailView(DetailView):\n@@ -19,6 +22,7 @@\n \n     def get_context_data(self, **kwargs):\n         source = self.get_object()\n+        user = self.request.user\n         display_unpublished = self.request.user.is_authenticated\n         if (source.published is False) and (not display_unpublished):\n             raise PermissionDenied()\n@@ -43,6 +47,9 @@\n             context[\"folios\"] = folios\n             # the options for the feast selector on the right, only chant sources have this\n             context[\"feasts_with_folios\"] = get_feast_selector_options(source, folios)\n+\n+        context[\"user_can_edit_chants\"] = user_can_edit_chants_in_source(user, source)\n+        context[\"user_can_edit_source\"] = user_can_edit_source(user, source)\n         return context\n \n \n@@ -246,24 +253,7 @@\n         source_id = self.kwargs.get(self.pk_url_kwarg)\n         source = get_object_or_404(Source, id=source_id)\n \n-        assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n-\n-        # checks if the user is a project manager\n-        is_project_manager = user.groups.filter(name=\"project manager\").exists()\n-        # checks if the user is an editor\n-        is_editor = user.groups.filter(name=\"editor\").exists()\n-        # checks if the user is a contributor\n-        is_contributor = user.groups.filter(name=\"contributor\").exists()\n-\n-        if (\n-            (is_project_manager)\n-            or (is_editor and assigned_to_source)\n-            or (is_editor and source.created_by == user)\n-            or (is_contributor and source.created_by == user)\n-        ):\n-            return True\n-        else:\n-            return False\n+        return user_can_edit_source(user, source)\n \n     def form_valid(self, form):\n         form.instance.last_updated_by = self.request.user\n@@ -284,3 +274,25 @@\n             new_editor.sources_user_can_edit.add(source)\n \n         return HttpResponseRedirect(self.get_success_url())\n+\n+\n+def user_can_edit_source(user, source):\n+    source_id = source.id\n+    assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n+\n+    # checks if the user is a project manager\n+    is_project_manager = user.groups.filter(name=\"project manager\").exists()\n+    # checks if the user is an editor\n+    is_editor = user.groups.filter(name=\"editor\").exists()\n+    # checks if the user is a contributor\n+    is_contributor = user.groups.filter(name=\"contributor\").exists()\n+\n+    if (\n+        (is_project_manager)\n+        or (is_editor and assigned_to_source)\n+        or (is_editor and source.created_by == user)\n+        or (is_contributor and source.created_by == user)\n+    ):\n+        return True\n+    else:\n+        return False\n", "issue": "Source Detail page should not display link to \"Manage source editors\" to all logged-in users\nCurrently, in the Source Detail view, if a user is logged in, we display \"add new chant\", \"full text & volpiano editor\", \"edit source description\" and \"manage source editors\" links in the sidebar. We need to check that the user has permission to edit the source before displaying these links, otherwise, users will click through into 403 errors. An extra check will be required for the \"manage source editors\" link - only people with \"staff status\" should see this link, as it leads to the admin area.\n", "before_files": [{"content": "from django.views.generic import DetailView, ListView, CreateView, UpdateView\nfrom django.db.models import Q, Prefetch\nfrom main_app.models import Source, Provenance, Century\nfrom main_app.forms import SourceCreateForm, SourceEditForm\nfrom django.contrib import messages\nfrom django.urls import reverse\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.http import HttpResponseRedirect\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.shortcuts import get_object_or_404\nfrom main_app.views.chant import get_feast_selector_options\n\n\nclass SourceDetailView(DetailView):\n    model = Source\n    context_object_name = \"source\"\n    template_name = \"source_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        source = self.get_object()\n        display_unpublished = self.request.user.is_authenticated\n        if (source.published is False) and (not display_unpublished):\n            raise PermissionDenied()\n\n        context = super().get_context_data(**kwargs)\n\n        if source.segment and source.segment.id == 4064:\n            # if this is a sequence source\n            context[\"sequences\"] = source.sequence_set.order_by(\"s_sequence\")\n            context[\"folios\"] = (\n                source.sequence_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n        else:\n            # if this is a chant source\n            folios = (\n                source.chant_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n            context[\"folios\"] = folios\n            # the options for the feast selector on the right, only chant sources have this\n            context[\"feasts_with_folios\"] = get_feast_selector_options(source, folios)\n        return context\n\n\nclass SourceListView(ListView):\n    paginate_by = 100\n    context_object_name = \"sources\"\n    template_name = \"source_list.html\"\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[\"provenances\"] = (\n            Provenance.objects.all().order_by(\"name\").values(\"id\", \"name\")\n        )\n        context[\"centuries\"] = (\n            Century.objects.all().order_by(\"name\").values(\"id\", \"name\")\n        )\n        return context\n\n    def get_queryset(self):\n        # use select_related() for foreign keys to reduce DB queries\n        queryset = Source.objects.select_related(\n            \"rism_siglum\", \"segment\", \"provenance\"\n        ).order_by(\"siglum\")\n\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            q_obj_filter = Q()\n        else:\n            q_obj_filter = Q(published=True)\n\n        if self.request.GET.get(\"century\"):\n            century_name = Century.objects.get(id=self.request.GET.get(\"century\")).name\n            q_obj_filter &= Q(century__name__icontains=century_name)\n\n        if self.request.GET.get(\"provenance\"):\n            provenance_id = int(self.request.GET.get(\"provenance\"))\n            q_obj_filter &= Q(provenance__id=provenance_id)\n        if self.request.GET.get(\"segment\"):\n            segment_id = int(self.request.GET.get(\"segment\"))\n            q_obj_filter &= Q(segment__id=segment_id)\n        if self.request.GET.get(\"fullSource\") in [\"true\", \"false\"]:\n            full_source_str = self.request.GET.get(\"fullSource\")\n            if full_source_str == \"true\":\n                full_source_q = Q(full_source=True) | Q(full_source=None)\n                q_obj_filter &= full_source_q\n            else:\n                q_obj_filter &= Q(full_source=False)\n\n        if self.request.GET.get(\"general\"):\n            # Strip spaces at the beginning and end. Then make list of terms split on spaces\n            general_search_terms = self.request.GET.get(\"general\").strip(\" \").split(\" \")\n            # We need a Q Object for each field we're gonna look into\n            title_q = Q()\n            siglum_q = Q()\n            rism_siglum_q = Q()\n            description_q = Q()\n            # it seems that old cantus don't look into title and provenance for the general search terms\n            # cantus.uwaterloo.ca/source/123901 this source cannot be found by searching its provenance 'Kremsm\u00fcnster' in the general search field\n            # provenance_q = Q()\n            summary_q = Q()\n\n            # For each term, add it to the Q object of each field with an OR operation.\n            # We split the terms so that the words can be separated in the actual\n            # field, allowing for a more flexible search, and a field needs\n            # to match only one of the terms\n            for term in general_search_terms:\n                title_q |= Q(title__icontains=term)\n                siglum_q |= Q(siglum__icontains=term)\n                rism_siglum_q |= Q(rism_siglum__name__icontains=term) | Q(\n                    rism_siglum__description__icontains=term\n                )\n                description_q |= Q(description__icontains=term)\n                summary_q |= Q(summary__icontains=term)\n                # provenance_q |= Q(provenance__name__icontains=term)\n            # All the Q objects are put together with OR.\n            # The end result is that at least one term has to match in at least one\n            # field\n            # general_search_q = (\n            #     title_q | siglum_q | rism_siglum_q | description_q | provenance_q\n            # )\n            general_search_q = (\n                title_q | siglum_q | rism_siglum_q | description_q | summary_q\n            )\n            q_obj_filter &= general_search_q\n\n        # For the indexing notes search we follow the same procedure as above but with\n        # different fields\n        if self.request.GET.get(\"indexing\"):\n            # Make list of terms split on spaces\n            indexing_search_terms = self.request.GET.get(\"indexing\").split(\" \")\n            # We need a Q Object for each field we're gonna look into\n            inventoried_by_q = Q()\n            full_text_entered_by_q = Q()\n            melodies_entered_by_q = Q()\n            proofreaders_q = Q()\n            other_editors_q = Q()\n            indexing_notes_q = Q()\n            # For each term, add it to the Q object of each field with an OR operation.\n            # We split the terms so that the words can be separated in the actual\n            # field, allowing for a more flexible search, and a field needs\n            # to match only one of the terms\n            for term in indexing_search_terms:\n                inventoried_by_q |= Q(inventoried_by__full_name__icontains=term)\n                full_text_entered_by_q |= Q(\n                    full_text_entered_by__full_name__icontains=term\n                )\n                melodies_entered_by_q |= Q(\n                    melodies_entered_by__full_name__icontains=term\n                )\n                proofreaders_q |= Q(proofreaders__full_name__icontains=term)\n                other_editors_q |= Q(other_editors__full_name__icontains=term)\n                indexing_notes_q |= Q(indexing_notes__icontains=term)\n            # All the Q objects are put together with OR.\n            # The end result is that at least one term has to match in at least one\n            # field\n            indexing_search_q = (\n                inventoried_by_q\n                | full_text_entered_by_q\n                | melodies_entered_by_q\n                | proofreaders_q\n                | other_editors_q\n                | indexing_notes_q\n            )\n            q_obj_filter &= indexing_search_q\n\n        return queryset.filter(q_obj_filter).prefetch_related(\n            Prefetch(\"century\", queryset=Century.objects.all().order_by(\"id\"))\n        )\n\n\nclass SourceCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):\n    model = Source\n    template_name = \"source_create_form.html\"\n    form_class = SourceCreateForm\n\n    def test_func(self):\n        user = self.request.user\n        # checks if the user is allowed to create sources\n        is_authorized = user.groups.filter(\n            Q(name=\"project manager\") | Q(name=\"editor\") | Q(name=\"contributor\")\n        ).exists()\n\n        if is_authorized:\n            return True\n        else:\n            return False\n\n    def get_success_url(self):\n        return reverse(\"source-detail\", args=[self.object.id])\n\n    def form_valid(self, form):\n        form.instance.created_by = self.request.user\n        self.object = form.save()\n\n        # assign this source to the \"current_editors\"\n        current_editors = self.object.current_editors.all()\n        self.request.user.sources_user_can_edit.add(self.object)\n\n        for editor in current_editors:\n            editor.sources_user_can_edit.add(self.object)\n\n        messages.success(\n            self.request,\n            \"Source created successfully!\",\n        )\n        return HttpResponseRedirect(self.get_success_url())\n\n\nclass SourceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):\n    template_name = \"source_edit.html\"\n    model = Source\n    form_class = SourceEditForm\n    pk_url_kwarg = \"source_id\"\n\n    def get_context_data(self, **kwargs):\n        source = self.get_object()\n        context = super().get_context_data(**kwargs)\n\n        if source.segment and source.segment.id == 4064:\n            # if this is a sequence source\n            context[\"sequences\"] = source.sequence_set.order_by(\"s_sequence\")\n            context[\"folios\"] = (\n                source.sequence_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n        else:\n            # if this is a chant source\n            folios = (\n                source.chant_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n            context[\"folios\"] = folios\n            # the options for the feast selector on the right, only chant sources have this\n            context[\"feasts_with_folios\"] = get_feast_selector_options(source, folios)\n        return context\n\n    def test_func(self):\n        user = self.request.user\n        source_id = self.kwargs.get(self.pk_url_kwarg)\n        source = get_object_or_404(Source, id=source_id)\n\n        assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n\n        # checks if the user is a project manager\n        is_project_manager = user.groups.filter(name=\"project manager\").exists()\n        # checks if the user is an editor\n        is_editor = user.groups.filter(name=\"editor\").exists()\n        # checks if the user is a contributor\n        is_contributor = user.groups.filter(name=\"contributor\").exists()\n\n        if (\n            (is_project_manager)\n            or (is_editor and assigned_to_source)\n            or (is_editor and source.created_by == user)\n            or (is_contributor and source.created_by == user)\n        ):\n            return True\n        else:\n            return False\n\n    def form_valid(self, form):\n        form.instance.last_updated_by = self.request.user\n\n        # remove this source from the old \"current_editors\"\n        # assign this source to the new \"current_editors\"\n\n        old_current_editors = list(\n            Source.objects.get(id=form.instance.id).current_editors.all()\n        )\n        new_current_editors = form.cleaned_data[\"current_editors\"]\n        source = form.save()\n\n        for old_editor in old_current_editors:\n            old_editor.sources_user_can_edit.remove(source)\n\n        for new_editor in new_current_editors:\n            new_editor.sources_user_can_edit.add(source)\n\n        return HttpResponseRedirect(self.get_success_url())\n", "path": "django/cantusdb_project/main_app/views/source.py"}], "after_files": [{"content": "from django.views.generic import DetailView, ListView, CreateView, UpdateView\nfrom django.db.models import Q, Prefetch\nfrom main_app.models import Source, Provenance, Century\nfrom main_app.forms import SourceCreateForm, SourceEditForm\nfrom django.contrib import messages\nfrom django.urls import reverse\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.http import HttpResponseRedirect\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.core.exceptions import PermissionDenied\nfrom django.shortcuts import get_object_or_404\nfrom main_app.views.chant import (\n    get_feast_selector_options,\n    user_can_edit_chants_in_source,\n)\n\n\nclass SourceDetailView(DetailView):\n    model = Source\n    context_object_name = \"source\"\n    template_name = \"source_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        source = self.get_object()\n        user = self.request.user\n        display_unpublished = self.request.user.is_authenticated\n        if (source.published is False) and (not display_unpublished):\n            raise PermissionDenied()\n\n        context = super().get_context_data(**kwargs)\n\n        if source.segment and source.segment.id == 4064:\n            # if this is a sequence source\n            context[\"sequences\"] = source.sequence_set.order_by(\"s_sequence\")\n            context[\"folios\"] = (\n                source.sequence_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n        else:\n            # if this is a chant source\n            folios = (\n                source.chant_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n            context[\"folios\"] = folios\n            # the options for the feast selector on the right, only chant sources have this\n            context[\"feasts_with_folios\"] = get_feast_selector_options(source, folios)\n\n        context[\"user_can_edit_chants\"] = user_can_edit_chants_in_source(user, source)\n        context[\"user_can_edit_source\"] = user_can_edit_source(user, source)\n        return context\n\n\nclass SourceListView(ListView):\n    paginate_by = 100\n    context_object_name = \"sources\"\n    template_name = \"source_list.html\"\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[\"provenances\"] = (\n            Provenance.objects.all().order_by(\"name\").values(\"id\", \"name\")\n        )\n        context[\"centuries\"] = (\n            Century.objects.all().order_by(\"name\").values(\"id\", \"name\")\n        )\n        return context\n\n    def get_queryset(self):\n        # use select_related() for foreign keys to reduce DB queries\n        queryset = Source.objects.select_related(\n            \"rism_siglum\", \"segment\", \"provenance\"\n        ).order_by(\"siglum\")\n\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            q_obj_filter = Q()\n        else:\n            q_obj_filter = Q(published=True)\n\n        if self.request.GET.get(\"century\"):\n            century_name = Century.objects.get(id=self.request.GET.get(\"century\")).name\n            q_obj_filter &= Q(century__name__icontains=century_name)\n\n        if self.request.GET.get(\"provenance\"):\n            provenance_id = int(self.request.GET.get(\"provenance\"))\n            q_obj_filter &= Q(provenance__id=provenance_id)\n        if self.request.GET.get(\"segment\"):\n            segment_id = int(self.request.GET.get(\"segment\"))\n            q_obj_filter &= Q(segment__id=segment_id)\n        if self.request.GET.get(\"fullSource\") in [\"true\", \"false\"]:\n            full_source_str = self.request.GET.get(\"fullSource\")\n            if full_source_str == \"true\":\n                full_source_q = Q(full_source=True) | Q(full_source=None)\n                q_obj_filter &= full_source_q\n            else:\n                q_obj_filter &= Q(full_source=False)\n\n        if self.request.GET.get(\"general\"):\n            # Strip spaces at the beginning and end. Then make list of terms split on spaces\n            general_search_terms = self.request.GET.get(\"general\").strip(\" \").split(\" \")\n            # We need a Q Object for each field we're gonna look into\n            title_q = Q()\n            siglum_q = Q()\n            rism_siglum_q = Q()\n            description_q = Q()\n            # it seems that old cantus don't look into title and provenance for the general search terms\n            # cantus.uwaterloo.ca/source/123901 this source cannot be found by searching its provenance 'Kremsm\u00fcnster' in the general search field\n            # provenance_q = Q()\n            summary_q = Q()\n\n            # For each term, add it to the Q object of each field with an OR operation.\n            # We split the terms so that the words can be separated in the actual\n            # field, allowing for a more flexible search, and a field needs\n            # to match only one of the terms\n            for term in general_search_terms:\n                title_q |= Q(title__icontains=term)\n                siglum_q |= Q(siglum__icontains=term)\n                rism_siglum_q |= Q(rism_siglum__name__icontains=term) | Q(\n                    rism_siglum__description__icontains=term\n                )\n                description_q |= Q(description__icontains=term)\n                summary_q |= Q(summary__icontains=term)\n                # provenance_q |= Q(provenance__name__icontains=term)\n            # All the Q objects are put together with OR.\n            # The end result is that at least one term has to match in at least one\n            # field\n            # general_search_q = (\n            #     title_q | siglum_q | rism_siglum_q | description_q | provenance_q\n            # )\n            general_search_q = (\n                title_q | siglum_q | rism_siglum_q | description_q | summary_q\n            )\n            q_obj_filter &= general_search_q\n\n        # For the indexing notes search we follow the same procedure as above but with\n        # different fields\n        if self.request.GET.get(\"indexing\"):\n            # Make list of terms split on spaces\n            indexing_search_terms = self.request.GET.get(\"indexing\").split(\" \")\n            # We need a Q Object for each field we're gonna look into\n            inventoried_by_q = Q()\n            full_text_entered_by_q = Q()\n            melodies_entered_by_q = Q()\n            proofreaders_q = Q()\n            other_editors_q = Q()\n            indexing_notes_q = Q()\n            # For each term, add it to the Q object of each field with an OR operation.\n            # We split the terms so that the words can be separated in the actual\n            # field, allowing for a more flexible search, and a field needs\n            # to match only one of the terms\n            for term in indexing_search_terms:\n                inventoried_by_q |= Q(inventoried_by__full_name__icontains=term)\n                full_text_entered_by_q |= Q(\n                    full_text_entered_by__full_name__icontains=term\n                )\n                melodies_entered_by_q |= Q(\n                    melodies_entered_by__full_name__icontains=term\n                )\n                proofreaders_q |= Q(proofreaders__full_name__icontains=term)\n                other_editors_q |= Q(other_editors__full_name__icontains=term)\n                indexing_notes_q |= Q(indexing_notes__icontains=term)\n            # All the Q objects are put together with OR.\n            # The end result is that at least one term has to match in at least one\n            # field\n            indexing_search_q = (\n                inventoried_by_q\n                | full_text_entered_by_q\n                | melodies_entered_by_q\n                | proofreaders_q\n                | other_editors_q\n                | indexing_notes_q\n            )\n            q_obj_filter &= indexing_search_q\n\n        return queryset.filter(q_obj_filter).prefetch_related(\n            Prefetch(\"century\", queryset=Century.objects.all().order_by(\"id\"))\n        )\n\n\nclass SourceCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):\n    model = Source\n    template_name = \"source_create_form.html\"\n    form_class = SourceCreateForm\n\n    def test_func(self):\n        user = self.request.user\n        # checks if the user is allowed to create sources\n        is_authorized = user.groups.filter(\n            Q(name=\"project manager\") | Q(name=\"editor\") | Q(name=\"contributor\")\n        ).exists()\n\n        if is_authorized:\n            return True\n        else:\n            return False\n\n    def get_success_url(self):\n        return reverse(\"source-detail\", args=[self.object.id])\n\n    def form_valid(self, form):\n        form.instance.created_by = self.request.user\n        self.object = form.save()\n\n        # assign this source to the \"current_editors\"\n        current_editors = self.object.current_editors.all()\n        self.request.user.sources_user_can_edit.add(self.object)\n\n        for editor in current_editors:\n            editor.sources_user_can_edit.add(self.object)\n\n        messages.success(\n            self.request,\n            \"Source created successfully!\",\n        )\n        return HttpResponseRedirect(self.get_success_url())\n\n\nclass SourceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):\n    template_name = \"source_edit.html\"\n    model = Source\n    form_class = SourceEditForm\n    pk_url_kwarg = \"source_id\"\n\n    def get_context_data(self, **kwargs):\n        source = self.get_object()\n        context = super().get_context_data(**kwargs)\n\n        if source.segment and source.segment.id == 4064:\n            # if this is a sequence source\n            context[\"sequences\"] = source.sequence_set.order_by(\"s_sequence\")\n            context[\"folios\"] = (\n                source.sequence_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n        else:\n            # if this is a chant source\n            folios = (\n                source.chant_set.values_list(\"folio\", flat=True)\n                .distinct()\n                .order_by(\"folio\")\n            )\n            context[\"folios\"] = folios\n            # the options for the feast selector on the right, only chant sources have this\n            context[\"feasts_with_folios\"] = get_feast_selector_options(source, folios)\n        return context\n\n    def test_func(self):\n        user = self.request.user\n        source_id = self.kwargs.get(self.pk_url_kwarg)\n        source = get_object_or_404(Source, id=source_id)\n\n        return user_can_edit_source(user, source)\n\n    def form_valid(self, form):\n        form.instance.last_updated_by = self.request.user\n\n        # remove this source from the old \"current_editors\"\n        # assign this source to the new \"current_editors\"\n\n        old_current_editors = list(\n            Source.objects.get(id=form.instance.id).current_editors.all()\n        )\n        new_current_editors = form.cleaned_data[\"current_editors\"]\n        source = form.save()\n\n        for old_editor in old_current_editors:\n            old_editor.sources_user_can_edit.remove(source)\n\n        for new_editor in new_current_editors:\n            new_editor.sources_user_can_edit.add(source)\n\n        return HttpResponseRedirect(self.get_success_url())\n\n\ndef user_can_edit_source(user, source):\n    source_id = source.id\n    assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n\n    # checks if the user is a project manager\n    is_project_manager = user.groups.filter(name=\"project manager\").exists()\n    # checks if the user is an editor\n    is_editor = user.groups.filter(name=\"editor\").exists()\n    # checks if the user is a contributor\n    is_contributor = user.groups.filter(name=\"contributor\").exists()\n\n    if (\n        (is_project_manager)\n        or (is_editor and assigned_to_source)\n        or (is_editor and source.created_by == user)\n        or (is_contributor and source.created_by == user)\n    ):\n        return True\n    else:\n        return False\n", "path": "django/cantusdb_project/main_app/views/source.py"}]}