From 4024ef8e350e7bd919654f75bb5f6d98f43a8de0 Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Tue, 19 Apr 2016 21:45:36 +0200
Subject: [PATCH] Install instance-level permissions for List, Team, Graph (and
 node)

---
 SciPost_v1/settings.py                        |  2 +
 comments/views.py                             | 10 ++--
 scipost/admin.py                              |  7 ++-
 .../commands/add_groups_and_permissions.py    | 40 +++++++-------
 scipost/models.py                             | 15 ++++++
 .../templates/scipost/edit_graph_node.html    | 11 +++-
 scipost/templates/scipost/graph.html          | 10 +++-
 scipost/templates/scipost/list.html           |  6 ++-
 scipost/templates/scipost/personal_page.html  |  6 +--
 scipost/views.py                              | 52 ++++++++++++-------
 submissions/views.py                          | 34 ++++++------
 templates/403.html                            | 19 +++++++
 templates/404.html                            | 19 +++++++
 templates/500.html                            | 16 ++++++
 theses/views.py                               |  6 +--
 15 files changed, 182 insertions(+), 71 deletions(-)
 create mode 100644 templates/403.html
 create mode 100644 templates/404.html
 create mode 100644 templates/500.html

diff --git a/SciPost_v1/settings.py b/SciPost_v1/settings.py
index 9146c5c78..044b28c16 100644
--- a/SciPost_v1/settings.py
+++ b/SciPost_v1/settings.py
@@ -46,6 +46,8 @@ AUTHENTICATION_BACKENDS = (
 
 LOGIN_URL = '/login/'
 
+GUARDIAN_RENDER_403 = True
+
 # Session expire at browser close                                                                  
 SESSION_EXPIRE_AT_BROWSER_CLOSE = True
 
diff --git a/comments/views.py b/comments/views.py
index d613f9a63..364c1abd9 100644
--- a/comments/views.py
+++ b/comments/views.py
@@ -16,7 +16,7 @@ from .forms import *
 from scipost.models import title_dict
 
 
-@permission_required('scipost.can_vet_comments')
+@permission_required('scipost.can_vet_comments', raise_exception=True)
 def vet_submitted_comments(request):
     contributor = Contributor.objects.get(user=request.user)
     comments_to_vet = Comment.objects.filter(status=0).order_by('date_submitted')
@@ -24,7 +24,7 @@ def vet_submitted_comments(request):
     context = {'contributor': contributor, 'comments_to_vet': comments_to_vet, 'form': form }
     return(render(request, 'comments/vet_submitted_comments.html', context))
 
-@permission_required('scipost.can_vet_comments')
+@permission_required('scipost.can_vet_comments', raise_exception=True)
 def vet_submitted_comment_ack(request, comment_id):
     if request.method == 'POST':
         form = VetCommentForm(request.POST)
@@ -89,7 +89,7 @@ def vet_submitted_comment_ack(request, comment_id):
     return render(request, 'comments/vet_submitted_comment_ack.html', context)
 
 
-@permission_required('scipost.can_submit_comments')
+@permission_required('scipost.can_submit_comments', raise_exception=True)
 def reply_to_comment(request, comment_id):
     comment = get_object_or_404(Comment, pk=comment_id)
     # Verify if this is from an author:
@@ -132,7 +132,7 @@ def reply_to_comment(request, comment_id):
     return render(request, 'comments/reply_to_comment.html', context)
 
 
-@permission_required('scipost.can_submit_comments')
+@permission_required('scipost.can_submit_comments', raise_exception=True)
 def reply_to_report(request, report_id):
     report = get_object_or_404(Report, pk=report_id)
     # Verify if this is from an author:
@@ -169,7 +169,7 @@ def reply_to_report(request, report_id):
     return render(request, 'comments/reply_to_report.html', context)
 
 
-@permission_required('scipost.can_express_opinion_on_comments')
+@permission_required('scipost.can_express_opinion_on_comments', raise_exception=True)
 def express_opinion(request, comment_id, opinion):
     # A contributor has expressed an opinion on a comment
     contributor = request.user.contributor
diff --git a/scipost/admin.py b/scipost/admin.py
index 048c4be62..d0ef3f8ae 100644
--- a/scipost/admin.py
+++ b/scipost/admin.py
@@ -3,6 +3,8 @@ from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User, Permission
 
+from guardian.admin import GuardedModelAdmin
+
 from scipost.models import *
 
 class ContributorInline(admin.StackedInline):
@@ -27,7 +29,10 @@ admin.site.register(AuthorshipClaim)
 
 admin.site.register(Permission)
 
-admin.site.register(List)
+class ListAdmin(GuardedModelAdmin):
+    search_fields = ['owner', 'title']
+
+admin.site.register(List, ListAdmin)
 
 admin.site.register(Team)
 
diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py
index 7662de762..ffdecba76 100644
--- a/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost/management/commands/add_groups_and_permissions.py
@@ -93,22 +93,21 @@ class Command(BaseCommand):
             name= 'Can act as a referee and submit reports on Submissions',
             content_type=content_type)
 
-        # Lists
-        can_create_list, created = Permission.objects.get_or_create(
-            codename='can_create_list',
-            name= 'Can create a new List',
-            content_type=content_type)
-        # Teams
-        can_create_team, created = Permission.objects.get_or_create(
-            codename='can_create_team',
-            name= 'Can create a new Team',
-            content_type=content_type)
-
-        # Graphs
-        can_create_graph, created = Permission.objects.get_or_create(
-            codename='can_create_graph',
-            name= 'Can create a new Graph',
-            content_type=content_type)
+#        # Lists
+#        can_create_list, created = Permission.objects.get_or_create(
+#            codename='can_create_list',
+#            name= 'Can create a new List',
+#            content_type=content_type)
+#        # Teams
+#        can_create_team, created = Permission.objects.get_or_create(
+#            codename='can_create_team',
+#            name= 'Can create a new Team',
+#            content_type=content_type)
+#        # Graphs
+#        can_create_graph, created = Permission.objects.get_or_create(
+#            codename='can_create_graph',
+#            name= 'Can create a new Graph',
+#            content_type=content_type)
 
 
 
@@ -135,9 +134,10 @@ class Command(BaseCommand):
                                                can_request_thesislinks,
                                                can_referee,
                                                )
-        Testers.permissions.add(can_create_list,
-                                can_create_team,
-                                can_create_graph,
-                                )
+        Testers.permissions.add(
+            can_create_list,
+            can_create_team,
+            can_create_graph,
+            )
 
         self.stdout.write(self.style.SUCCESS('Successfully created groups and permissions'))
diff --git a/scipost/models.py b/scipost/models.py
index 29b8e09ab..70043eb9e 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -274,6 +274,9 @@ class List(models.Model):
     thesislinks = models.ManyToManyField('theses.ThesisLink', blank=True, related_name='list_thesislinks')
     comments = models.ManyToManyField('comments.Comment', blank=True, related_name='list_comments')
 
+    class Meta:
+        default_permissions = ['add', 'view', 'change', 'delete']
+
 
     def __str__(self):
         return self.title[:30] + ' (owner: ' + self.owner.user.first_name + ' ' + self.owner.user.last_name + ')'
@@ -347,6 +350,10 @@ class Team(models.Model):
     name = models.CharField(max_length=100)
     established = models.DateField(default=timezone.now)
 
+    class Meta:
+        default_permissions = ['add', 'view', 'change', 'delete']
+
+
     def __str__(self):
         return self.name + ' (led by ' + self.leader.user.first_name + ' ' + self.leader.user.last_name + ')'
 
@@ -381,6 +388,10 @@ class Graph(models.Model):
     description = models.TextField(blank=True, null=True)
     created = models.DateTimeField(default=timezone.now)
 
+    class Meta:
+        default_permissions = ['add', 'view', 'change', 'delete']
+
+
     def __str__(self):
         return self.title[:30] + ' (owner: ' + self.owner.user.first_name + ' ' + self.owner.user.last_name + ')'
 
@@ -416,6 +427,10 @@ class Node(models.Model):
     commentaries = models.ManyToManyField('commentaries.Commentary', blank=True, related_name='node_commentaries')
     thesislinks = models.ManyToManyField('theses.ThesisLink', blank=True, related_name='node_thesislinks')
 
+    class Meta:
+        default_permissions = ['add', 'view', 'change', 'delete']
+
+
     def __str__(self):
         return self.graph.title[:20] + ': ' + self.name[:20] 
 
diff --git a/scipost/templates/scipost/edit_graph_node.html b/scipost/templates/scipost/edit_graph_node.html
index 0c252137c..2ff59425f 100644
--- a/scipost/templates/scipost/edit_graph_node.html
+++ b/scipost/templates/scipost/edit_graph_node.html
@@ -6,13 +6,22 @@
 
 <section>
   <h1>Edit Graph Node</h1>
-  <form action="{% url 'scipost:edit_graph_node' node_id=node.id %}" method="post">
+
+  {% if errormessage %}
+  <p>{{ errormessage }}</p>
+
+  {% else %}
+
+  <form action="{% url 'scipost:edit_graph_node' graph_id=graph.id node_id=node.id %}" method="post">
     {% csrf_token %}
     <table>
       {{ edit_node_form.as_table }}
     </table>
     <input type="submit" value="Submit" />
   </form>
+
+  {% endif %}
+
 </section>
 
 {% endblock bodysup %}
diff --git a/scipost/templates/scipost/graph.html b/scipost/templates/scipost/graph.html
index ca0a8406e..cc3b66c52 100644
--- a/scipost/templates/scipost/graph.html
+++ b/scipost/templates/scipost/graph.html
@@ -164,6 +164,11 @@ function transform(d) {
   </ul>
   {% endif %}
 
+
+  {% load guardian_tags %}
+  {% get_obj_perms request.user for graph as "graph_perms" %}
+  {% if "change_graph" in graph_perms %}
+
   <div class="row">
     <div class="col-3">
       {% if graph.private %}
@@ -197,12 +202,15 @@ function transform(d) {
   </div>
   <hr class="hr6"/>
   <br/>
+
+  {% endif %}
+
   {{ graph.contents }}
 
 
   {% for node in nodes %}
   {{ node.contents }}
-  <a href="{% url 'scipost:edit_graph_node' node_id=node.id %}" class="node_contents node_id{{ node.id}}">Edit this Node's contents</a>
+  <a href="{% url 'scipost:edit_graph_node' graph_id=graph.id node_id=node.id %}" class="node_contents node_id{{ node.id}}">Edit this Node's contents</a>
   {% endfor %}
 
 
diff --git a/scipost/templates/scipost/list.html b/scipost/templates/scipost/list.html
index 65859053e..61a2d56fa 100644
--- a/scipost/templates/scipost/list.html
+++ b/scipost/templates/scipost/list.html
@@ -18,8 +18,11 @@
   <br/>
   {{ list.contents }}
 
-  <hr class="hr6"/>
+  {% load guardian_tags %}
+  {% get_obj_perms request.user for list as "list_perms" %}
+  {% if "change_list" in list_perms %}
 
+  <hr class="hr6"/>
   <h3>Add elements to this List</h3>
   <form action="{% url 'scipost:list' list_id=list.id %}" method="post">
     {% csrf_token %}
@@ -93,6 +96,7 @@
   </ul>
   {% endif %}
 
+  {% endif %}
 </section>
 
 {% endblock bodysup %}
diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html
index 16277d140..2c69feda4 100644
--- a/scipost/templates/scipost/personal_page.html
+++ b/scipost/templates/scipost/personal_page.html
@@ -111,13 +111,13 @@
     <li><a class="TabItem" id="ThesesTab">Theses</a></li>
     <li><a class="TabItem" id="CommentsTab">Comments</a></li>
     <li><a class="TabItem" id="AuthorRepliesTab">Author Replies</a></li>
-    {% if perms.scipost.can_create_list %}
+    {% if request.user|is_in_group:'Testers' %}
     <li><a class="TabItem" id="ListsTab">Lists</a></li>
     {% endif %}
-    {% if perms.scipost.can_create_team %}
+    {% if request.user|is_in_group:'Testers' %}
     <li><a class="TabItem" id="TeamsTab">Teams</a></li>
     {% endif %}
-    {% if perms.scipost.can_create_graph %}
+    {% if request.user|is_in_group:'Testers' %}
     <li><a class="TabItem" id="GraphsTab">Graphs</a></li>
     {% endif %}
   </ul>
diff --git a/scipost/views.py b/scipost/views.py
index 3d35d6d40..c8cdfabae 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -7,7 +7,8 @@ import string
 from django.utils import timezone
 from django.shortcuts import get_object_or_404, render
 from django.contrib.auth import authenticate, login, logout
-from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.decorators import login_required 
+#from django.contrib.auth.decorators import permission_required   # Superseded by guardian
 from django.contrib.auth.models import User, Group, Permission
 from django.contrib.auth.views import password_reset, password_reset_confirm
 from django.core.mail import EmailMessage
@@ -19,6 +20,8 @@ from django.utils.http import is_safe_url
 from django.views.decorators.csrf import csrf_protect
 from django.db.models import Avg
 
+from guardian.decorators import permission_required, permission_required_or_403
+from guardian.shortcuts import assign_perm
 
 from .models import *
 from .forms import *
@@ -276,7 +279,7 @@ def request_new_activation_link(request, oldkey):
     return render (request, 'scipost/request_new_activation_link_ack.html')
 
 
-@permission_required('scipost.can_vet_registration_requests')
+@permission_required('scipost.can_vet_registration_requests', return_403=True)
 def vet_registration_requests(request):
     contributor = Contributor.objects.get(user=request.user)
     contributors_to_vet = Contributor.objects.filter(user__is_active=True, status=0).order_by('key_expires')
@@ -285,7 +288,7 @@ def vet_registration_requests(request):
     context = {'contributors_to_vet': contributors_to_vet, 'form': form }
     return render(request, 'scipost/vet_registration_requests.html', context)
 
-@permission_required('scipost.can_vet_registration_requests')
+@permission_required('scipost.can_vet_registration_requests', return_403=True)
 def vet_registration_request_ack(request, contributor_id):
     # process the form
     if request.method == 'POST':
@@ -335,7 +338,7 @@ def vet_registration_request_ack(request, contributor_id):
     return render(request, 'scipost/vet_registration_request_ack.html', context)
 
 
-@permission_required('scipost.can_manage_registration_invitations')
+@permission_required('scipost.can_manage_registration_invitations', return_403=True)
 def registration_invitations(request):
     # List invitations sent; send new ones
     errormessage = ''
@@ -377,8 +380,6 @@ def login_view(request):
     redirect_to = (redirect_to 
                    if is_safe_url(redirect_to, request.get_host()) 
                    else reverse('scipost:personal_page'))
-    if request.user.is_authenticated: # for users who navigate back to pre-logged in page
-        return redirect(redirect_to)
     if request.method == 'POST':
         username = request.POST['username']
         password = request.POST['password']
@@ -637,13 +638,13 @@ def claim_thesis_authorship(request, thesis_id, claim):
     return redirect('scipost:claim_authorships')
 
 
-@permission_required('scipost.can_vet_authorship_claims')
+@permission_required('scipost.can_vet_authorship_claims', return_403=True)
 def vet_authorship_claims(request):
     claims_to_vet = AuthorshipClaim.objects.filter(status='0')
     context = {'claims_to_vet': claims_to_vet}
     return render(request, 'scipost/vet_authorship_claims.html', context)
 
-@permission_required('scipost.can_vet_authorship_claims')
+@permission_required('scipost.can_vet_authorship_claims', return_403=True)
 def vet_authorship_claim(request, claim_id, claim):
     if request.method == 'POST':
         vetting_contributor = Contributor.objects.get(user=request.user)
@@ -708,7 +709,7 @@ def contributor_info(request, contributor_id):
 # Lists #
 #########
 
-@permission_required('scipost.can_create_list')
+@permission_required('scipost.add_list', return_403=True)
 def create_list(request):
     listcreated = False
     message = None
@@ -722,6 +723,9 @@ def create_list(request):
                            created=timezone.now())
             newlist.save()
             listcreated = True
+            assign_perm('scipost.change_list', request.user, newlist)
+            assign_perm('scipost.view_list', request.user, newlist)
+            assign_perm('scipost.delete_list', request.user, newlist)
             message = 'List ' + create_list_form.cleaned_data['title'] + ' was successfully created.'
     else:
         create_list_form = CreateListForm()
@@ -730,7 +734,7 @@ def create_list(request):
     return render(request, 'scipost/create_list.html', context)
 
 
-@permission_required('scipost.can_create_list')
+@permission_required_or_403('scipost.view_list', (List, 'id', 'list_id'))
 def list(request, list_id):
     list = get_object_or_404(List, pk=list_id)
     context = {'list': list}
@@ -744,7 +748,7 @@ def list(request, list_id):
     return render(request, 'scipost/list.html', context)
 
 
-@permission_required('scipost.can_create_list')
+@permission_required_or_403('scipost.change_list', (List, 'id', 'list_id'))
 def list_add_element(request, list_id, type, element_id):
     list = get_object_or_404(List, pk=list_id)
     if type == 'C':
@@ -766,7 +770,7 @@ def list_add_element(request, list_id, type, element_id):
 # Teams #
 #########
 
-@permission_required('scipost.can_create_team')
+@permission_required('scipost.add_team', return_403=True)
 def create_team(request):
     if request.method == "POST":
         create_team_form = CreateTeamForm(request.POST)
@@ -775,6 +779,9 @@ def create_team(request):
                            name=create_team_form.cleaned_data['name'],
                            established=timezone.now())
             newteam.save()
+            assign_perm('scipost.change_team', request.user, newteam)
+            assign_perm('scipost.view_team', request.user, newteam)
+            assign_perm('scipost.delete_team', request.user, newteam)
             return redirect(reverse('scipost:add_team_member', kwargs={'team_id': newteam.id}))
     else:
         create_team_form = CreateTeamForm()
@@ -783,7 +790,7 @@ def create_team(request):
                'add_team_member_form': add_team_member_form}
     return render(request, 'scipost/create_team.html', context)
 
-@permission_required('scipost.can_create_team')
+@permission_required_or_403('scipost.change_team', (Team, 'id', 'team_id'))
 def add_team_member(request, team_id, contributor_id=None):
     team = get_object_or_404(Team, pk=team_id)
     contributors_found = None
@@ -791,6 +798,7 @@ def add_team_member(request, team_id, contributor_id=None):
         contributor = get_object_or_404(Contributor, pk=contributor_id)
         team.members.add(contributor)
         team.save()
+        assign_perm('scipost.view_team', contributor.user, team)
         return redirect(reverse('scipost:add_team_member', kwargs={'team_id': team_id}))
     if request.method == "POST":
         add_team_member_form = AddTeamMemberForm(request.POST)
@@ -807,7 +815,7 @@ def add_team_member(request, team_id, contributor_id=None):
 # Graphs #
 ##########
 
-@permission_required('scipost.can_create_graph')
+@permission_required('scipost.add_graph', return_403=True)
 def create_graph(request):
     graphcreated = False
     message = None
@@ -820,6 +828,9 @@ def create_graph(request):
                              private=create_graph_form.cleaned_data['private'],
                              created=timezone.now())
             newgraph.save()
+            assign_perm('scipost.change_graph', request.user, newgraph)
+            assign_perm('scipost.view_graph', request.user, newgraph)
+            assign_perm('scipost.delete_graph', request.user, newgraph)
             graphcreated = True
             message = 'Graph ' + create_graph_form.cleaned_data['title'] + ' was successfully created.'
     else:
@@ -829,7 +840,7 @@ def create_graph(request):
     return render(request, 'scipost/create_graph.html', context)
 
 
-@permission_required('scipost.can_create_graph')
+@permission_required_or_403('scipost.view_graph', (Graph, 'id', 'graph_id'))
 def graph(request, graph_id):
     graph = get_object_or_404(Graph, pk=graph_id)
     nodes = Node.objects.filter(graph=graph)
@@ -868,10 +879,12 @@ def graph(request, graph_id):
     return render(request, 'scipost/graph.html', context)
 
 
-@permission_required('scipost.can_create_graph')
 def edit_graph_node(request, node_id):
     node = get_object_or_404(Node, pk=node_id)
-    if request.method == "POST":
+    errormessage = ''
+    if not request.user.has_perm('scipost.change_graph', node.graph):
+        errormessage = 'You do not have permission to edit this graph.'
+    elif request.method == "POST":
         edit_node_form = CreateNodeForm(request.POST, instance=node)
         if edit_node_form.is_valid():
             node.name=edit_node_form.cleaned_data['name']
@@ -884,11 +897,12 @@ def edit_graph_node(request, node_id):
             return redirect(reverse('scipost:graph', kwargs={'graph_id': node.graph.id}), context)
     else:
         edit_node_form = CreateNodeForm(instance=node)
-    context = {'graph': graph, 'node': node, 'edit_node_form': edit_node_form}
+    context = {'graph': graph, 'node': node, 'edit_node_form': edit_node_form,
+               'errormessage': errormessage}
     return render(request, 'scipost/edit_graph_node.html', context)
 
 
-@login_required
+@permission_required_or_403('scipost.view_graph', (Graph, 'id', 'graph_id'))
 def api_graph(request, graph_id):
     """ Produce JSON data to plot graph """
     graph = get_object_or_404(Graph, pk=graph_id)
diff --git a/submissions/views.py b/submissions/views.py
index bb5900030..d2f56dd99 100644
--- a/submissions/views.py
+++ b/submissions/views.py
@@ -26,7 +26,7 @@ from comments.forms import CommentForm
 # SUBMISSIONS:
 ###############
 
-@permission_required('scipost.can_submit_manuscript')
+@permission_required('scipost.can_submit_manuscript', raise_exception=True)
 def submit_manuscript(request):
     if request.method == 'POST':
         form = SubmissionForm(request.POST)
@@ -152,7 +152,7 @@ def submission_detail(request, submission_id):
 # Editorial workflow #
 ######################
 
-@permission_required('scipost.can_assign_submissions')
+@permission_required('scipost.can_assign_submissions', raise_exception=True)
 def assign_submissions(request):
     submission_to_assign = Submission.objects.filter(status='unassigned').first() # only handle one at at time
     if submission_to_assign is not None:
@@ -164,7 +164,7 @@ def assign_submissions(request):
     return render(request, 'submissions/assign_submissions.html', context)
 
 
-@permission_required('scipost.can_assign_submissions')
+@permission_required('scipost.can_assign_submissions', raise_exception=True)
 def assign_submission_ack(request, submission_id):
     submission = Submission.objects.get(pk=submission_id)
     if request.method == 'POST':
@@ -183,7 +183,7 @@ def assign_submission_ack(request, submission_id):
     return render(request, 'submissions/assign_submission_ack.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def accept_or_decline_assignments(request):
     contributor = Contributor.objects.get(user=request.user)
     assignment = EditorialAssignment.objects.filter(to=contributor, accepted=None).first()
@@ -192,7 +192,7 @@ def accept_or_decline_assignments(request):
     return render(request, 'submissions/accept_or_decline_assignments.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def accept_or_decline_assignment_ack(request, assignment_id):
     contributor = Contributor.objects.get(user=request.user)
     assignment = get_object_or_404 (EditorialAssignment, pk=assignment_id)
@@ -219,7 +219,7 @@ def accept_or_decline_assignment_ack(request, assignment_id):
     return render(request, 'submissions/accept_or_decline_assignment_ack.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def editorial_page(request, submission_id):
     submission = get_object_or_404(Submission, pk=submission_id)
     ref_invitations = RefereeInvitation.objects.filter(submission=submission)
@@ -229,7 +229,7 @@ def editorial_page(request, submission_id):
     return render(request, 'submissions/editorial_page.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def select_referee(request, submission_id):
     submission = get_object_or_404(Submission, pk=submission_id)
     if request.method == 'POST':
@@ -245,7 +245,7 @@ def select_referee(request, submission_id):
     return render(request, 'submissions/select_referee.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def recruit_referee(request, submission_id):
     """
     If the Editor-in-charge does not find the desired referee among Contributors,
@@ -291,7 +291,7 @@ def recruit_referee(request, submission_id):
     return redirect(reverse('submissions:editorial_page', kwargs={'submission_id': submission_id}))
 
             
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def send_refereeing_invitation(request, submission_id, contributor_id):
     submission = get_object_or_404(Submission, pk=submission_id)
     contributor = get_object_or_404(Contributor, pk=contributor_id)
@@ -305,7 +305,7 @@ def send_refereeing_invitation(request, submission_id, contributor_id):
     return redirect(reverse('submissions:editorial_page', kwargs={'submission_id': submission_id}))
 
 
-@permission_required('scipost.can_referee')
+@permission_required('scipost.can_referee', raise_exception=True)
 def accept_or_decline_ref_invitations(request):
     contributor = Contributor.objects.get(user=request.user)
     invitation = RefereeInvitation.objects.filter(referee=contributor, accepted=None).first()
@@ -314,7 +314,7 @@ def accept_or_decline_ref_invitations(request):
     return render(request, 'submissions/accept_or_decline_ref_invitations.html', context)
 
 
-@permission_required('scipost.can_referee')
+@permission_required('scipost.can_referee', raise_exception=True)
 def accept_or_decline_ref_invitation_ack(request, invitation_id):
     contributor = Contributor.objects.get(user=request.user)
     invitation = get_object_or_404 (RefereeInvitation, pk=invitation_id)
@@ -333,7 +333,7 @@ def accept_or_decline_ref_invitation_ack(request, invitation_id):
     return render(request, 'submissions/accept_or_decline_ref_invitation_ack.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def extend_refereeing_deadline(request, submission_id, days):
     submission = get_object_or_404 (Submission, pk=submission_id)
     submission.reporting_deadline += datetime.timedelta(days=int(days))
@@ -341,7 +341,7 @@ def extend_refereeing_deadline(request, submission_id, days):
     return redirect(reverse('submissions:editorial_page', kwargs={'submission_id': submission_id}))
     
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def close_refereeing_round(request, submission_id):
     submission = get_object_or_404 (Submission, pk=submission_id)
     submission.open_for_reporting = False
@@ -375,7 +375,7 @@ def communication(request, submission_id, type, referee_id=None):
     return render(request, 'submissions/communication.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def eic_recommendation(request, submission_id):
     submission = get_object_or_404 (Submission, pk=submission_id)
     if request.method == 'POST':
@@ -399,7 +399,7 @@ def eic_recommendation(request, submission_id):
 # Reports
 ###########
 
-@permission_required('scipost.can_referee')
+@permission_required('scipost.can_referee', raise_exception=True)
 def submit_report(request, submission_id):
     submission = get_object_or_404 (Submission, pk=submission_id)
     if request.method == 'POST':
@@ -443,7 +443,7 @@ def submit_report(request, submission_id):
     return render(request, 'submissions/submit_report.html', context)
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def vet_submitted_reports(request):
     contributor = Contributor.objects.get(user=request.user)
     report_to_vet = Report.objects.filter(status=0).first() # only handle one at a time
@@ -452,7 +452,7 @@ def vet_submitted_reports(request):
     return(render(request, 'submissions/vet_submitted_reports.html', context))
 
 
-@permission_required('scipost.can_take_charge_of_submissions')
+@permission_required('scipost.can_take_charge_of_submissions', raise_exception=True)
 def vet_submitted_report_ack(request, report_id):
     if request.method == 'POST':
         form = VetReportForm(request.POST)
diff --git a/templates/403.html b/templates/403.html
new file mode 100644
index 000000000..126e12579
--- /dev/null
+++ b/templates/403.html
@@ -0,0 +1,19 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: 403{% endblock pagetitle %}
+
+{% block headsup %}
+
+{% endblock headsup %}
+
+{% block bodysup %}
+
+
+<section>
+  <h1>You are not authorized to view the requested page.</h1>
+  {% if exception %}
+  <p>Exception: {{ exception }}</p>
+  {% endif %}
+</section>
+
+{% endblock bodysup %}
diff --git a/templates/404.html b/templates/404.html
new file mode 100644
index 000000000..3402bd83c
--- /dev/null
+++ b/templates/404.html
@@ -0,0 +1,19 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: 403{% endblock pagetitle %}
+
+{% block headsup %}
+
+{% endblock headsup %}
+
+{% block bodysup %}
+
+
+<section>
+  <h1>The page you requested could not be found.</h1>
+  {% if exception %}
+  <p>Exception: {{ exception }}</p>
+  {% endif %}
+</section>
+
+{% endblock bodysup %}
diff --git a/templates/500.html b/templates/500.html
new file mode 100644
index 000000000..4ab810c9d
--- /dev/null
+++ b/templates/500.html
@@ -0,0 +1,16 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: 403{% endblock pagetitle %}
+
+{% block headsup %}
+
+{% endblock headsup %}
+
+{% block bodysup %}
+
+
+<section>
+  <h1>The server responded with an error.</h1>
+</section>
+
+{% endblock bodysup %}
diff --git a/theses/views.py b/theses/views.py
index 03b748e77..701cf5848 100644
--- a/theses/views.py
+++ b/theses/views.py
@@ -24,7 +24,7 @@ title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails
 ################
 
 
-@permission_required('scipost.can_request_thesislink')
+@permission_required('scipost.can_request_thesislink', raise_exception=True)
 def request_thesislink(request):
     if request.method == 'POST':
         form = RequestThesisLinkForm(request.POST)
@@ -52,7 +52,7 @@ def request_thesislink(request):
     return render(request, 'theses/request_thesislink.html', {'form': form})
 
 
-@permission_required('scipost.can_vet_thesislink_requests')
+@permission_required('scipost.can_vet_thesislink_requests', raise_exception=True)
 def vet_thesislink_requests(request):
     contributor = Contributor.objects.get(user=request.user)
     thesislink_to_vet = ThesisLink.objects.filter(vetted=False).first() # only handle one at a time
@@ -60,7 +60,7 @@ def vet_thesislink_requests(request):
     context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form }
     return render(request, 'theses/vet_thesislink_requests.html', context)
 
-@permission_required('scipost.can_vet_thesislink_requests')
+@permission_required('scipost.can_vet_thesislink_requests', raise_exception=True)
 def vet_thesislink_request_ack(request, thesislink_id):
     if request.method == 'POST':
         form = VetThesisLinkForm(request.POST)
-- 
GitLab