Repository: DDMAL/CantusDB

Issue:
Users should only be shown the View-Edit toggle if they have edit access for the source/chant in question
@annamorphism made a comment on #441 that really deserves its own issue

> also is there a way to not have the "Edit" tab show up for unauthorized people? it's annoying to try to edit something and then be sent to a 403.

Currently, the view-edit toggle is being displayed whenever the user is logged in. Instead, we need to properly check that the user is actually allowed to edit the chant before displaying the Edit link.


Discussion:
I will also add here, as I did there, that it would be good to make sure that in doing so we aren't restricting edit capabilities more than we want to. e.g. are users who on OldCantus are allowed to fix a quick typo in any chant now only allowed to do so if the creator of the chant grants them access? and if so, is that a good thing (fewer unauthorized edits) or a bad one (every tiny correction has to go through an admin)?  
I only have the one Debra-level account on OldCantus, so I can't check what various levels of user are allowed to do as it stands. 

Resolving patch:
diff --git a/django/cantusdb_project/main_app/views/chant.py b/django/cantusdb_project/main_app/views/chant.py
--- a/django/cantusdb_project/main_app/views/chant.py
+++ b/django/cantusdb_project/main_app/views/chant.py
@@ -63,12 +63,15 @@ class ChantDetailView(DetailView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         chant = self.get_object()
+        user = self.request.user
         
         # if the chant's source isn't published, only logged-in users should be able to view the chant's detail page
         source = chant.source
-        display_unpublished = self.request.user.is_authenticated
+        display_unpublished = user.is_authenticated
         if (source.published is False) and (not display_unpublished):
             raise PermissionDenied()
+        
+        context["user_can_edit_chant"] = user_can_edit_chants_in_source(user, source)
 
         # syllabification section
         if chant.volpiano:
@@ -1028,23 +1031,7 @@ def test_func(self):
         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 assigned_to_source)
-            or (is_contributor and source.created_by == user)):
-            return True
-        else:
-            return False
+        return user_can_edit_chants_in_source(user, source)
 
     # if success_url and get_success_url not specified, will direct to chant detail page
     def get_success_url(self):
@@ -1255,24 +1242,8 @@ def test_func(self):
         chant_id = self.kwargs.get(self.pk_url_kwarg)
         chant = get_object_or_404(Chant, id=chant_id)
         source = chant.source
-
-        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 assigned_to_source)
-            or (is_contributor and source.created_by == user)):
-            return True
-        else:
-            return False
+        
+        return user_can_edit_chants_in_source(user, source)
 
     def get_success_url(self):
         return reverse("source-edit-chants", args=[self.object.source.id])
@@ -1367,23 +1338,7 @@ def test_func(self):
         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 assigned_to_source)
-            or (is_contributor and source.created_by == user)):
-            return True
-        else:
-            return False
+        return user_can_edit_chants_in_source(user, source)
 
     def get_queryset(self):
         """
@@ -1693,6 +1648,8 @@ class ChantProofreadView(SourceEditChantsView):
 
     def test_func(self):
         user = self.request.user
+        if user.is_anonymous:
+            return False
         source_id = self.kwargs.get(self.pk_url_kwarg)
 
         assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
@@ -1719,23 +1676,7 @@ def test_func(self):
         source = chant.source
         user = self.request.user
 
-        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 assigned_to_source)
-            or (is_contributor and source.created_by == user)):
-            return True
-        else:
-            return False
+        return user_can_edit_chants_in_source(user, source)
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
@@ -1779,3 +1720,20 @@ def form_valid(self, form):
     def get_success_url(self):
         # stay on the same page after save
         return self.request.get_full_path()
+
+def user_can_edit_chants_in_source(user, source):
+    if user.is_anonymous:
+        return False
+    
+    source_id = source.id
+    user_is_assigned_to_source = user.sources_user_can_edit.filter(id=source_id)
+
+    user_is_project_manager = user.groups.filter(name="project manager").exists()
+    user_is_editor = user.groups.filter(name="editor").exists()
+    user_is_contributor = 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))
\ No newline at end of file


Test patch: