We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Users assigned to Bower sources should be able to edit sequences in those sources
Currently, only project managers can edit sequences. Debra requests that editors should be able to be assigned to Bower sources and edit the sequences contained therein.

--- 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/sequence.py`
Content:
```
1 from django.views.generic import DetailView, ListView, UpdateView
2 from main_app.models import Sequence
3 from django.db.models import Q
4 from main_app.forms import SequenceEditForm
5 from django.contrib.auth.mixins import LoginRequiredMixin
6 from django.contrib import messages
7 from django.contrib.auth.mixins import UserPassesTestMixin
8 from django.core.exceptions import PermissionDenied
9 from main_app.permissions import (
10     user_can_view_sequence,
11     user_can_edit_sequences,
12 )
13 
14 
15 class SequenceDetailView(DetailView):
16     """
17     Displays a single Sequence object. Accessed with ``sequences/<int:pk>``
18     """
19 
20     model = Sequence
21     context_object_name = "sequence"
22     template_name = "sequence_detail.html"
23 
24     def get_context_data(self, **kwargs):
25         sequence = self.get_object()
26         user = self.request.user
27 
28         # if the sequence's source isn't published,
29         # only logged-in users should be able to view the sequence's detail page
30         if not user_can_view_sequence(user, sequence):
31             raise PermissionDenied()
32 
33         context = super().get_context_data(**kwargs)
34         context["concordances"] = (
35             Sequence.objects.filter(cantus_id=sequence.cantus_id)
36             .select_related("source")
37             .order_by("siglum")
38         )
39         context["user_can_edit_sequence"] = user_can_edit_sequences(user)
40         return context
41 
42 
43 class SequenceListView(ListView):
44     """
45     Displays a list of Sequence objects. Accessed with ``sequences/``
46     """
47 
48     paginate_by = 100
49     context_object_name = "sequences"
50     template_name = "sequence_list.html"
51 
52     def get_queryset(self):
53         queryset = Sequence.objects.select_related("source")
54         display_unpublished = self.request.user.is_authenticated
55         if display_unpublished:
56             q_obj_filter = Q()
57         else:
58             q_obj_filter = Q(source__published=True)
59 
60         if self.request.GET.get("incipit"):
61             incipit = self.request.GET.get("incipit")
62             q_obj_filter &= Q(incipit__icontains=incipit)
63         if self.request.GET.get("siglum"):
64             siglum = self.request.GET.get("siglum")
65             q_obj_filter &= Q(siglum__icontains=siglum)
66         if self.request.GET.get("cantus_id"):
67             cantus_id = self.request.GET.get("cantus_id")
68             q_obj_filter &= Q(cantus_id__icontains=cantus_id)
69 
70         return queryset.filter(q_obj_filter).order_by("siglum", "s_sequence")
71 
72 
73 class SequenceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
74     template_name = "sequence_edit.html"
75     model = Sequence
76     form_class = SequenceEditForm
77     pk_url_kwarg = "sequence_id"
78 
79     def form_valid(self, form):
80         form.instance.last_updated_by = self.request.user
81         messages.success(
82             self.request,
83             "Sequence updated successfully!",
84         )
85         return super().form_valid(form)
86 
87     def test_func(self):
88         user = self.request.user
89         return user_can_edit_sequences(user)
90
```

Path: `django/cantusdb_project/main_app/permissions.py`
Content:
```
1 from django.db.models import Q
2 from typing import Optional
3 from main_app.models import (
4     Source,
5     Chant,
6     Sequence,
7 )
8 from users.models import User
9 
10 
11 def user_can_edit_chants_in_source(user: User, source: Optional[Source]) -> bool:
12     """
13     Checks if the user can edit Chants in a given Source.
14     Used in ChantDetail, ChantList, ChantCreate, ChantDelete, ChantEdit,
15     ChantEditSyllabification, and SourceDetail views.
16     """
17     if user.is_anonymous or (source is None):
18         return False
19 
20     source_id = source.id
21     user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(
22         id=source_id
23     ).exists()
24 
25     user_is_project_manager: bool = user.groups.filter(name="project manager").exists()
26     user_is_editor: bool = user.groups.filter(name="editor").exists()
27     user_is_contributor: bool = user.groups.filter(name="contributor").exists()
28 
29     return (
30         (user_is_project_manager)
31         or (user_is_editor and user_is_assigned_to_source)
32         or (user_is_editor and source.created_by == user)
33         or (user_is_contributor and user_is_assigned_to_source)
34         or (user_is_contributor and source.created_by == user)
35     )
36 
37 
38 def user_can_proofread_chant(user: User, chant: Chant) -> bool:
39     """
40     Checks if the user can access the proofreading page of a given Source.
41     Used in SourceEditChantsView.
42     """
43     source_id = chant.source.id
44     if user.is_anonymous:
45         return False
46     user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(
47         id=source_id
48     ).exists()
49 
50     user_is_project_manager: bool = user.groups.filter(name="project manager").exists()
51     user_is_editor: bool = user.groups.filter(name="editor").exists()
52 
53     return (user_is_project_manager) or (user_is_editor and user_is_assigned_to_source)
54 
55 
56 def user_can_view_source(user: User, source: Source) -> bool:
57     """
58     Checks if the user can view an unpublished Source on the site.
59     Used in ChantDetail, SequenceDetail, and SourceDetail views.
60     """
61     user_is_authenticated: bool = user.is_authenticated
62     return (source.published) or (user_is_authenticated)
63 
64 
65 def user_can_view_chant(user: User, chant: Chant) -> bool:
66     """
67     Checks if the user can view a Chant belonging to an unpublished Source on the site.
68     Used in ChantDetail, SequenceDetail, and SourceDetail views.
69     """
70     source = chant.source
71     user_is_authenticated: bool = user.is_authenticated
72     return (source is not None) and ((source.published) or (user_is_authenticated))
73 
74 
75 def user_can_view_sequence(user: User, sequence: Sequence) -> bool:
76     """
77     Checks if the user can view a Sequence belonging to an unpublished Source on the site.
78     Used in ChantDetail, SequenceDetail, and SourceDetail views.
79     """
80     source = sequence.source
81     user_is_authenticated: bool = user.is_authenticated
82     return (source is not None) and ((source.published) or (user_is_authenticated))
83 
84 
85 def user_can_edit_sequences(user: User) -> bool:
86     """
87     Checks if the user has permission to edit a Sequence object.
88     Used in SequenceDetail and SequenceEdit views.
89     """
90     return user.groups.filter(name="project manager").exists()
91 
92 
93 def user_can_create_sources(user: User) -> bool:
94     """
95     Checks if the user has permission to create a Source object.
96     Used in SourceCreateView.
97     """
98 
99     return user.groups.filter(
100         Q(name="project manager") | Q(name="editor") | Q(name="contributor")
101     ).exists()
102 
103 
104 def user_can_edit_source(user: User, source: Source) -> bool:
105     """
106     Checks if the user has permission to edit a Source object.
107     Used in SourceDetail, SourceEdit, and SourceDelete views.
108     """
109     if user.is_anonymous:
110         return False
111     source_id = source.id
112     assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
113 
114     is_project_manager: bool = user.groups.filter(name="project manager").exists()
115     is_editor: bool = user.groups.filter(name="editor").exists()
116     is_contributor: bool = user.groups.filter(name="contributor").exists()
117 
118     return (
119         (is_project_manager)
120         or (is_editor and assigned_to_source)
121         or (is_editor and source.created_by == user)
122         or (is_contributor and source.created_by == user)
123     )
124 
125 
126 def user_can_view_user_detail(viewing_user: User, user: User) -> bool:
127     """
128     Checks if the user can view the user detail pages of regular users in the database or just indexers.
129     Used in UserDetailView.
130     """
131     return viewing_user.is_authenticated or user.is_indexer
132 
133 
134 def user_can_manage_source_editors(user: User) -> bool:
135     """
136     Checks if the user has permission to change the editors assigned to a Source.
137     Used in SourceDetailView.
138     """
139     return user.is_staff or user.groups.filter(name="project manager").exists()
140
```
--- 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/permissions.py b/django/cantusdb_project/main_app/permissions.py
--- a/django/cantusdb_project/main_app/permissions.py
+++ b/django/cantusdb_project/main_app/permissions.py
@@ -82,12 +82,31 @@
     return (source is not None) and ((source.published) or (user_is_authenticated))
 
 
-def user_can_edit_sequences(user: User) -> bool:
+def user_can_edit_sequences(user: User, sequence: Sequence) -> bool:
     """
     Checks if the user has permission to edit a Sequence object.
     Used in SequenceDetail and SequenceEdit views.
     """
-    return user.groups.filter(name="project manager").exists()
+    source = sequence.source
+    if user.is_anonymous or (source is None):
+        return False
+
+    source_id = source.id
+    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(
+        id=source_id
+    ).exists()
+
+    user_is_project_manager: bool = user.groups.filter(name="project manager").exists()
+    user_is_editor: bool = user.groups.filter(name="editor").exists()
+    user_is_contributor: bool = user.groups.filter(name="contributor").exists()
+
+    return (
+        (user_is_project_manager)
+        or (user_is_editor and user_is_assigned_to_source)
+        or (user_is_editor and source.created_by == user)
+        or (user_is_contributor and user_is_assigned_to_source)
+        or (user_is_contributor and source.created_by == user)
+    )
 
 
 def user_can_create_sources(user: User) -> bool:
diff --git a/django/cantusdb_project/main_app/views/sequence.py b/django/cantusdb_project/main_app/views/sequence.py
--- a/django/cantusdb_project/main_app/views/sequence.py
+++ b/django/cantusdb_project/main_app/views/sequence.py
@@ -36,7 +36,7 @@
             .select_related("source")
             .order_by("siglum")
         )
-        context["user_can_edit_sequence"] = user_can_edit_sequences(user)
+        context["user_can_edit_sequence"] = user_can_edit_sequences(user, sequence)
         return context
 
 
@@ -86,4 +86,5 @@
 
     def test_func(self):
         user = self.request.user
-        return user_can_edit_sequences(user)
+        sequence = self.get_object()
+        return user_can_edit_sequences(user, sequence)

{"golden_diff": "diff --git a/django/cantusdb_project/main_app/permissions.py b/django/cantusdb_project/main_app/permissions.py\n--- a/django/cantusdb_project/main_app/permissions.py\n+++ b/django/cantusdb_project/main_app/permissions.py\n@@ -82,12 +82,31 @@\n     return (source is not None) and ((source.published) or (user_is_authenticated))\n \n \n-def user_can_edit_sequences(user: User) -> bool:\n+def user_can_edit_sequences(user: User, sequence: Sequence) -> bool:\n     \"\"\"\n     Checks if the user has permission to edit a Sequence object.\n     Used in SequenceDetail and SequenceEdit views.\n     \"\"\"\n-    return user.groups.filter(name=\"project manager\").exists()\n+    source = sequence.source\n+    if user.is_anonymous or (source is None):\n+        return False\n+\n+    source_id = source.id\n+    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n+        id=source_id\n+    ).exists()\n+\n+    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n+    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n+    user_is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n+\n+    return (\n+        (user_is_project_manager)\n+        or (user_is_editor and user_is_assigned_to_source)\n+        or (user_is_editor and source.created_by == user)\n+        or (user_is_contributor and user_is_assigned_to_source)\n+        or (user_is_contributor and source.created_by == user)\n+    )\n \n \n def user_can_create_sources(user: User) -> bool:\ndiff --git a/django/cantusdb_project/main_app/views/sequence.py b/django/cantusdb_project/main_app/views/sequence.py\n--- a/django/cantusdb_project/main_app/views/sequence.py\n+++ b/django/cantusdb_project/main_app/views/sequence.py\n@@ -36,7 +36,7 @@\n             .select_related(\"source\")\n             .order_by(\"siglum\")\n         )\n-        context[\"user_can_edit_sequence\"] = user_can_edit_sequences(user)\n+        context[\"user_can_edit_sequence\"] = user_can_edit_sequences(user, sequence)\n         return context\n \n \n@@ -86,4 +86,5 @@\n \n     def test_func(self):\n         user = self.request.user\n-        return user_can_edit_sequences(user)\n+        sequence = self.get_object()\n+        return user_can_edit_sequences(user, sequence)\n", "issue": "Users assigned to Bower sources should be able to edit sequences in those sources\nCurrently, only project managers can edit sequences. Debra requests that editors should be able to be assigned to Bower sources and edit the sequences contained therein.\n", "before_files": [{"content": "from django.views.generic import DetailView, ListView, UpdateView\nfrom main_app.models import Sequence\nfrom django.db.models import Q\nfrom main_app.forms import SequenceEditForm\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.contrib import messages\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.core.exceptions import PermissionDenied\nfrom main_app.permissions import (\n    user_can_view_sequence,\n    user_can_edit_sequences,\n)\n\n\nclass SequenceDetailView(DetailView):\n    \"\"\"\n    Displays a single Sequence object. Accessed with ``sequences/<int:pk>``\n    \"\"\"\n\n    model = Sequence\n    context_object_name = \"sequence\"\n    template_name = \"sequence_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        sequence = self.get_object()\n        user = self.request.user\n\n        # if the sequence's source isn't published,\n        # only logged-in users should be able to view the sequence's detail page\n        if not user_can_view_sequence(user, sequence):\n            raise PermissionDenied()\n\n        context = super().get_context_data(**kwargs)\n        context[\"concordances\"] = (\n            Sequence.objects.filter(cantus_id=sequence.cantus_id)\n            .select_related(\"source\")\n            .order_by(\"siglum\")\n        )\n        context[\"user_can_edit_sequence\"] = user_can_edit_sequences(user)\n        return context\n\n\nclass SequenceListView(ListView):\n    \"\"\"\n    Displays a list of Sequence objects. Accessed with ``sequences/``\n    \"\"\"\n\n    paginate_by = 100\n    context_object_name = \"sequences\"\n    template_name = \"sequence_list.html\"\n\n    def get_queryset(self):\n        queryset = Sequence.objects.select_related(\"source\")\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            q_obj_filter = Q()\n        else:\n            q_obj_filter = Q(source__published=True)\n\n        if self.request.GET.get(\"incipit\"):\n            incipit = self.request.GET.get(\"incipit\")\n            q_obj_filter &= Q(incipit__icontains=incipit)\n        if self.request.GET.get(\"siglum\"):\n            siglum = self.request.GET.get(\"siglum\")\n            q_obj_filter &= Q(siglum__icontains=siglum)\n        if self.request.GET.get(\"cantus_id\"):\n            cantus_id = self.request.GET.get(\"cantus_id\")\n            q_obj_filter &= Q(cantus_id__icontains=cantus_id)\n\n        return queryset.filter(q_obj_filter).order_by(\"siglum\", \"s_sequence\")\n\n\nclass SequenceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):\n    template_name = \"sequence_edit.html\"\n    model = Sequence\n    form_class = SequenceEditForm\n    pk_url_kwarg = \"sequence_id\"\n\n    def form_valid(self, form):\n        form.instance.last_updated_by = self.request.user\n        messages.success(\n            self.request,\n            \"Sequence updated successfully!\",\n        )\n        return super().form_valid(form)\n\n    def test_func(self):\n        user = self.request.user\n        return user_can_edit_sequences(user)\n", "path": "django/cantusdb_project/main_app/views/sequence.py"}, {"content": "from django.db.models import Q\nfrom typing import Optional\nfrom main_app.models import (\n    Source,\n    Chant,\n    Sequence,\n)\nfrom users.models import User\n\n\ndef user_can_edit_chants_in_source(user: User, source: Optional[Source]) -> bool:\n    \"\"\"\n    Checks if the user can edit Chants in a given Source.\n    Used in ChantDetail, ChantList, ChantCreate, ChantDelete, ChantEdit,\n    ChantEditSyllabification, and SourceDetail views.\n    \"\"\"\n    if user.is_anonymous or (source is None):\n        return False\n\n    source_id = source.id\n    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n        id=source_id\n    ).exists()\n\n    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n    user_is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n\n    return (\n        (user_is_project_manager)\n        or (user_is_editor and user_is_assigned_to_source)\n        or (user_is_editor and source.created_by == user)\n        or (user_is_contributor and user_is_assigned_to_source)\n        or (user_is_contributor and source.created_by == user)\n    )\n\n\ndef user_can_proofread_chant(user: User, chant: Chant) -> bool:\n    \"\"\"\n    Checks if the user can access the proofreading page of a given Source.\n    Used in SourceEditChantsView.\n    \"\"\"\n    source_id = chant.source.id\n    if user.is_anonymous:\n        return False\n    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n        id=source_id\n    ).exists()\n\n    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n\n    return (user_is_project_manager) or (user_is_editor and user_is_assigned_to_source)\n\n\ndef user_can_view_source(user: User, source: Source) -> bool:\n    \"\"\"\n    Checks if the user can view an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    user_is_authenticated: bool = user.is_authenticated\n    return (source.published) or (user_is_authenticated)\n\n\ndef user_can_view_chant(user: User, chant: Chant) -> bool:\n    \"\"\"\n    Checks if the user can view a Chant belonging to an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    source = chant.source\n    user_is_authenticated: bool = user.is_authenticated\n    return (source is not None) and ((source.published) or (user_is_authenticated))\n\n\ndef user_can_view_sequence(user: User, sequence: Sequence) -> bool:\n    \"\"\"\n    Checks if the user can view a Sequence belonging to an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    source = sequence.source\n    user_is_authenticated: bool = user.is_authenticated\n    return (source is not None) and ((source.published) or (user_is_authenticated))\n\n\ndef user_can_edit_sequences(user: User) -> bool:\n    \"\"\"\n    Checks if the user has permission to edit a Sequence object.\n    Used in SequenceDetail and SequenceEdit views.\n    \"\"\"\n    return user.groups.filter(name=\"project manager\").exists()\n\n\ndef user_can_create_sources(user: User) -> bool:\n    \"\"\"\n    Checks if the user has permission to create a Source object.\n    Used in SourceCreateView.\n    \"\"\"\n\n    return user.groups.filter(\n        Q(name=\"project manager\") | Q(name=\"editor\") | Q(name=\"contributor\")\n    ).exists()\n\n\ndef user_can_edit_source(user: User, source: Source) -> bool:\n    \"\"\"\n    Checks if the user has permission to edit a Source object.\n    Used in SourceDetail, SourceEdit, and SourceDelete views.\n    \"\"\"\n    if user.is_anonymous:\n        return False\n    source_id = source.id\n    assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n\n    is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    is_editor: bool = user.groups.filter(name=\"editor\").exists()\n    is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n\n    return (\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\n\ndef user_can_view_user_detail(viewing_user: User, user: User) -> bool:\n    \"\"\"\n    Checks if the user can view the user detail pages of regular users in the database or just indexers.\n    Used in UserDetailView.\n    \"\"\"\n    return viewing_user.is_authenticated or user.is_indexer\n\n\ndef user_can_manage_source_editors(user: User) -> bool:\n    \"\"\"\n    Checks if the user has permission to change the editors assigned to a Source.\n    Used in SourceDetailView.\n    \"\"\"\n    return user.is_staff or user.groups.filter(name=\"project manager\").exists()\n", "path": "django/cantusdb_project/main_app/permissions.py"}], "after_files": [{"content": "from django.views.generic import DetailView, ListView, UpdateView\nfrom main_app.models import Sequence\nfrom django.db.models import Q\nfrom main_app.forms import SequenceEditForm\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.contrib import messages\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.core.exceptions import PermissionDenied\nfrom main_app.permissions import (\n    user_can_view_sequence,\n    user_can_edit_sequences,\n)\n\n\nclass SequenceDetailView(DetailView):\n    \"\"\"\n    Displays a single Sequence object. Accessed with ``sequences/<int:pk>``\n    \"\"\"\n\n    model = Sequence\n    context_object_name = \"sequence\"\n    template_name = \"sequence_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        sequence = self.get_object()\n        user = self.request.user\n\n        # if the sequence's source isn't published,\n        # only logged-in users should be able to view the sequence's detail page\n        if not user_can_view_sequence(user, sequence):\n            raise PermissionDenied()\n\n        context = super().get_context_data(**kwargs)\n        context[\"concordances\"] = (\n            Sequence.objects.filter(cantus_id=sequence.cantus_id)\n            .select_related(\"source\")\n            .order_by(\"siglum\")\n        )\n        context[\"user_can_edit_sequence\"] = user_can_edit_sequences(user, sequence)\n        return context\n\n\nclass SequenceListView(ListView):\n    \"\"\"\n    Displays a list of Sequence objects. Accessed with ``sequences/``\n    \"\"\"\n\n    paginate_by = 100\n    context_object_name = \"sequences\"\n    template_name = \"sequence_list.html\"\n\n    def get_queryset(self):\n        queryset = Sequence.objects.select_related(\"source\")\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            q_obj_filter = Q()\n        else:\n            q_obj_filter = Q(source__published=True)\n\n        if self.request.GET.get(\"incipit\"):\n            incipit = self.request.GET.get(\"incipit\")\n            q_obj_filter &= Q(incipit__icontains=incipit)\n        if self.request.GET.get(\"siglum\"):\n            siglum = self.request.GET.get(\"siglum\")\n            q_obj_filter &= Q(siglum__icontains=siglum)\n        if self.request.GET.get(\"cantus_id\"):\n            cantus_id = self.request.GET.get(\"cantus_id\")\n            q_obj_filter &= Q(cantus_id__icontains=cantus_id)\n\n        return queryset.filter(q_obj_filter).order_by(\"siglum\", \"s_sequence\")\n\n\nclass SequenceEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):\n    template_name = \"sequence_edit.html\"\n    model = Sequence\n    form_class = SequenceEditForm\n    pk_url_kwarg = \"sequence_id\"\n\n    def form_valid(self, form):\n        form.instance.last_updated_by = self.request.user\n        messages.success(\n            self.request,\n            \"Sequence updated successfully!\",\n        )\n        return super().form_valid(form)\n\n    def test_func(self):\n        user = self.request.user\n        sequence = self.get_object()\n        return user_can_edit_sequences(user, sequence)\n", "path": "django/cantusdb_project/main_app/views/sequence.py"}, {"content": "from django.db.models import Q\nfrom typing import Optional\nfrom main_app.models import (\n    Source,\n    Chant,\n    Sequence,\n)\nfrom users.models import User\n\n\ndef user_can_edit_chants_in_source(user: User, source: Optional[Source]) -> bool:\n    \"\"\"\n    Checks if the user can edit Chants in a given Source.\n    Used in ChantDetail, ChantList, ChantCreate, ChantDelete, ChantEdit,\n    ChantEditSyllabification, and SourceDetail views.\n    \"\"\"\n    if user.is_anonymous or (source is None):\n        return False\n\n    source_id = source.id\n    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n        id=source_id\n    ).exists()\n\n    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n    user_is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n\n    return (\n        (user_is_project_manager)\n        or (user_is_editor and user_is_assigned_to_source)\n        or (user_is_editor and source.created_by == user)\n        or (user_is_contributor and user_is_assigned_to_source)\n        or (user_is_contributor and source.created_by == user)\n    )\n\n\ndef user_can_proofread_chant(user: User, chant: Chant) -> bool:\n    \"\"\"\n    Checks if the user can access the proofreading page of a given Source.\n    Used in SourceEditChantsView.\n    \"\"\"\n    source_id = chant.source.id\n    if user.is_anonymous:\n        return False\n    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n        id=source_id\n    ).exists()\n\n    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n\n    return (user_is_project_manager) or (user_is_editor and user_is_assigned_to_source)\n\n\ndef user_can_view_source(user: User, source: Source) -> bool:\n    \"\"\"\n    Checks if the user can view an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    user_is_authenticated: bool = user.is_authenticated\n    return (source.published) or (user_is_authenticated)\n\n\ndef user_can_view_chant(user: User, chant: Chant) -> bool:\n    \"\"\"\n    Checks if the user can view a Chant belonging to an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    source = chant.source\n    user_is_authenticated: bool = user.is_authenticated\n    return (source is not None) and ((source.published) or (user_is_authenticated))\n\n\ndef user_can_view_sequence(user: User, sequence: Sequence) -> bool:\n    \"\"\"\n    Checks if the user can view a Sequence belonging to an unpublished Source on the site.\n    Used in ChantDetail, SequenceDetail, and SourceDetail views.\n    \"\"\"\n    source = sequence.source\n    user_is_authenticated: bool = user.is_authenticated\n    return (source is not None) and ((source.published) or (user_is_authenticated))\n\n\ndef user_can_edit_sequences(user: User, sequence: Sequence) -> bool:\n    \"\"\"\n    Checks if the user has permission to edit a Sequence object.\n    Used in SequenceDetail and SequenceEdit views.\n    \"\"\"\n    source = sequence.source\n    if user.is_anonymous or (source is None):\n        return False\n\n    source_id = source.id\n    user_is_assigned_to_source: bool = user.sources_user_can_edit.filter(\n        id=source_id\n    ).exists()\n\n    user_is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    user_is_editor: bool = user.groups.filter(name=\"editor\").exists()\n    user_is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n\n    return (\n        (user_is_project_manager)\n        or (user_is_editor and user_is_assigned_to_source)\n        or (user_is_editor and source.created_by == user)\n        or (user_is_contributor and user_is_assigned_to_source)\n        or (user_is_contributor and source.created_by == user)\n    )\n\n\ndef user_can_create_sources(user: User) -> bool:\n    \"\"\"\n    Checks if the user has permission to create a Source object.\n    Used in SourceCreateView.\n    \"\"\"\n\n    return user.groups.filter(\n        Q(name=\"project manager\") | Q(name=\"editor\") | Q(name=\"contributor\")\n    ).exists()\n\n\ndef user_can_edit_source(user: User, source: Source) -> bool:\n    \"\"\"\n    Checks if the user has permission to edit a Source object.\n    Used in SourceDetail, SourceEdit, and SourceDelete views.\n    \"\"\"\n    if user.is_anonymous:\n        return False\n    source_id = source.id\n    assigned_to_source = user.sources_user_can_edit.filter(id=source_id)\n\n    is_project_manager: bool = user.groups.filter(name=\"project manager\").exists()\n    is_editor: bool = user.groups.filter(name=\"editor\").exists()\n    is_contributor: bool = user.groups.filter(name=\"contributor\").exists()\n\n    return (\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\n\ndef user_can_view_user_detail(viewing_user: User, user: User) -> bool:\n    \"\"\"\n    Checks if the user can view the user detail pages of regular users in the database or just indexers.\n    Used in UserDetailView.\n    \"\"\"\n    return viewing_user.is_authenticated or user.is_indexer\n\n\ndef user_can_manage_source_editors(user: User) -> bool:\n    \"\"\"\n    Checks if the user has permission to change the editors assigned to a Source.\n    Used in SourceDetailView.\n    \"\"\"\n    return user.is_staff or user.groups.filter(name=\"project manager\").exists()\n", "path": "django/cantusdb_project/main_app/permissions.py"}]}