From 886c178ff052a73d22c352b6e802e60e0a0088f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-S=C3=A9bastien=20Caux?= <git@jscaux.org>
Date: Tue, 22 Nov 2022 05:54:44 +0100
Subject: [PATCH] Initiate edadmin app

---
 scipost_django/SciPost_v1/settings/base.py    | 22 ++++++----
 scipost_django/SciPost_v1/urls.py             |  1 +
 scipost_django/edadmin/__init__.py            |  0
 scipost_django/edadmin/admin.py               |  3 ++
 scipost_django/edadmin/apps.py                | 10 +++++
 scipost_django/edadmin/migrations/__init__.py |  0
 scipost_django/edadmin/models.py              |  3 ++
 .../edadmin/templates/edadmin/edadmin.html    | 27 ++++++++++++
 scipost_django/edadmin/tests.py               |  3 ++
 scipost_django/edadmin/urls.py                | 15 +++++++
 scipost_django/edadmin/views.py               | 21 +++++++++
 .../0115_alter_submission_options.py          | 17 ++++++++
 ...rmission_submissionuserobjectpermission.py | 43 +++++++++++++++++++
 .../submissions/models/submission.py          | 19 ++++++++
 scipost_django/submissions/urls.py            |  5 ++-
 15 files changed, 178 insertions(+), 11 deletions(-)
 create mode 100644 scipost_django/edadmin/__init__.py
 create mode 100644 scipost_django/edadmin/admin.py
 create mode 100644 scipost_django/edadmin/apps.py
 create mode 100644 scipost_django/edadmin/migrations/__init__.py
 create mode 100644 scipost_django/edadmin/models.py
 create mode 100644 scipost_django/edadmin/templates/edadmin/edadmin.html
 create mode 100644 scipost_django/edadmin/tests.py
 create mode 100644 scipost_django/edadmin/urls.py
 create mode 100644 scipost_django/edadmin/views.py
 create mode 100644 scipost_django/submissions/migrations/0115_alter_submission_options.py
 create mode 100644 scipost_django/submissions/migrations/0116_submissiongroupobjectpermission_submissionuserobjectpermission.py

diff --git a/scipost_django/SciPost_v1/settings/base.py b/scipost_django/SciPost_v1/settings/base.py
index 8f0e006de..f2177804a 100644
--- a/scipost_django/SciPost_v1/settings/base.py
+++ b/scipost_django/SciPost_v1/settings/base.py
@@ -73,6 +73,7 @@ os.environ["wsgi.url_scheme"] = "https"
 INSTALLED_APPS = [
     "dal",  # django-autocomplete-light
     "dal_select2",  # dal with Select2
+    # Django
     "django.contrib.admin",
     "django.contrib.admindocs",
     "django.contrib.auth",
@@ -82,12 +83,23 @@ INSTALLED_APPS = [
     "django.contrib.messages",
     "django.contrib.staticfiles",
     "django.contrib.sites",
+    # external
+    "corsheaders",
     "crispy_forms",
     "crispy_bootstrap5",
+    "django_celery_results",
+    "django_celery_beat",
     "django_countries",
     "django_extensions",
     "django_filters",
+    "guardian",
     "haystack",
+    "maintenancemode",
+    "oauth2_provider",
+    "rest_framework",
+    "sitesserved",
+    "webpack_loader",
+    # own apps
     "affiliates",
     "api",
     "apimail",
@@ -97,14 +109,11 @@ INSTALLED_APPS = [
     "comments",
     "common",
     "conflicts",
-    "corsheaders",
-    "django_celery_results",
-    "django_celery_beat",
+    "edadmin",
     "finances",
     "forums",
     "funders",
     "guides",
-    "guardian",
     "helpdesk",
     "invitations",
     "journals",
@@ -112,7 +121,6 @@ INSTALLED_APPS = [
     "mails",
     "markup",
     "news",
-    "oauth2_provider",
     "ontology",
     "organizations",
     "petitions",
@@ -120,7 +128,6 @@ INSTALLED_APPS = [
     "proceedings",
     "production",
     "profiles",
-    "rest_framework",
     "scipost",
     "security",
     "series",
@@ -128,10 +135,7 @@ INSTALLED_APPS = [
     "stats",
     "submissions",
     "theses",
-    "sitesserved",
     "webinars",
-    "webpack_loader",
-    "maintenancemode",
 ]
 
 SITE_ID = 1
diff --git a/scipost_django/SciPost_v1/urls.py b/scipost_django/SciPost_v1/urls.py
index 8c60cfb21..709938678 100644
--- a/scipost_django/SciPost_v1/urls.py
+++ b/scipost_django/SciPost_v1/urls.py
@@ -92,6 +92,7 @@ urlpatterns = [
     path("commentaries/", include("commentaries.urls", namespace="commentaries")),
     path("commentary/", include("commentaries.urls", namespace="_commentaries")),
     path("comments/", include("comments.urls", namespace="comments")),
+    path("edadmin/", include("edadmin.urls", namespace="edadmin")),
     path("forums/", include("forums.urls", namespace="forums")),
     path("funders/", include("funders.urls", namespace="funders")),
     path("finances/", include("finances.urls", namespace="finances")),
diff --git a/scipost_django/edadmin/__init__.py b/scipost_django/edadmin/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scipost_django/edadmin/admin.py b/scipost_django/edadmin/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/scipost_django/edadmin/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/scipost_django/edadmin/apps.py b/scipost_django/edadmin/apps.py
new file mode 100644
index 000000000..8edea9546
--- /dev/null
+++ b/scipost_django/edadmin/apps.py
@@ -0,0 +1,10 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.apps import AppConfig
+
+
+class EdadminConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'edadmin'
diff --git a/scipost_django/edadmin/migrations/__init__.py b/scipost_django/edadmin/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/scipost_django/edadmin/models.py b/scipost_django/edadmin/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/scipost_django/edadmin/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/scipost_django/edadmin/templates/edadmin/edadmin.html b/scipost_django/edadmin/templates/edadmin/edadmin.html
new file mode 100644
index 000000000..75ccdabd3
--- /dev/null
+++ b/scipost_django/edadmin/templates/edadmin/edadmin.html
@@ -0,0 +1,27 @@
+{% extends 'scipost/base.html' %}
+
+{% block breadcrumb %}
+  <div class="breadcrumb-container">
+    <div class="container">
+      <nav class="breadcrumb hidden-sm-down">
+        {% block breadcrumb_items %}
+          <a href="{% url 'submissions:pool' %}" class="breadcrumb-item">EdAdmin</a>
+        {% endblock %}
+      </nav>
+    </div>
+  </div>
+{% endblock %}
+
+{% block pagetitle %}: EdAdmin{% endblock pagetitle %}
+
+{% block content %}
+
+  <h2 class="highlight">Prescreening</h2>
+  {% for submission in prescreening %}
+    <details>
+      <summary>{{ submission }}</summary>
+      {{ submission }}
+    </details>
+  {% endfor %}
+
+{% endblock content %}
diff --git a/scipost_django/edadmin/tests.py b/scipost_django/edadmin/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/scipost_django/edadmin/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/scipost_django/edadmin/urls.py b/scipost_django/edadmin/urls.py
new file mode 100644
index 000000000..9caf5624b
--- /dev/null
+++ b/scipost_django/edadmin/urls.py
@@ -0,0 +1,15 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.urls import path, re_path
+from django.views.generic import TemplateView
+
+from . import views
+
+app_name = "edadmin"
+
+
+urlpatterns = [
+    path("", views.edadmin, name="edadmin"),
+]
diff --git a/scipost_django/edadmin/views.py b/scipost_django/edadmin/views.py
new file mode 100644
index 000000000..92ab47aee
--- /dev/null
+++ b/scipost_django/edadmin/views.py
@@ -0,0 +1,21 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.contrib.auth.decorators import login_required, user_passes_test
+from django.shortcuts import render
+
+from guardian.shortcuts import get_objects_for_user
+
+from scipost.permissions import is_edadmin
+
+
+@login_required
+@user_passes_test(is_edadmin)
+def edadmin(request):
+    """
+    Editorial administration page.
+    """
+    submissions = get_objects_for_user(request.user, "submissions.take_edadmin_actions")
+    context = { "prescreening": submissions.prescreening() }
+    return render(request, "edadmin/edadmin.html", context)
diff --git a/scipost_django/submissions/migrations/0115_alter_submission_options.py b/scipost_django/submissions/migrations/0115_alter_submission_options.py
new file mode 100644
index 000000000..28d73f0f6
--- /dev/null
+++ b/scipost_django/submissions/migrations/0115_alter_submission_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.16 on 2022-11-22 04:39
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('submissions', '0114_submission_internal_plagiarism_matches'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='submission',
+            options={'ordering': ['-submission_date'], 'permissions': [('take_edadmin_actions', 'Take editorial admin actions'), ('view_edadmin_info', 'View editorial admin information')]},
+        ),
+    ]
diff --git a/scipost_django/submissions/migrations/0116_submissiongroupobjectpermission_submissionuserobjectpermission.py b/scipost_django/submissions/migrations/0116_submissiongroupobjectpermission_submissionuserobjectpermission.py
new file mode 100644
index 000000000..95d8123ff
--- /dev/null
+++ b/scipost_django/submissions/migrations/0116_submissiongroupobjectpermission_submissionuserobjectpermission.py
@@ -0,0 +1,43 @@
+# Generated by Django 3.2.16 on 2022-11-22 04:44
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('auth', '0012_alter_user_first_name_max_length'),
+        ('submissions', '0115_alter_submission_options'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SubmissionUserObjectPermission',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submissions.submission')),
+                ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'abstract': False,
+                'unique_together': {('user', 'permission', 'content_object')},
+            },
+        ),
+        migrations.CreateModel(
+            name='SubmissionGroupObjectPermission',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='submissions.submission')),
+                ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
+                ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
+            ],
+            options={
+                'abstract': False,
+                'unique_together': {('group', 'permission', 'content_object')},
+            },
+        ),
+    ]
diff --git a/scipost_django/submissions/models/submission.py b/scipost_django/submissions/models/submission.py
index ad344779d..12af62b23 100644
--- a/scipost_django/submissions/models/submission.py
+++ b/scipost_django/submissions/models/submission.py
@@ -13,6 +13,9 @@ from django.urls import reverse
 from django.utils import timezone
 from django.utils.functional import cached_property
 
+from guardian.models import UserObjectPermissionBase
+from guardian.models import GroupObjectPermissionBase
+
 from scipost.behaviors import TimeStampedModel
 from scipost.constants import SCIPOST_APPROACHES
 from scipost.fields import ChoiceArrayField
@@ -196,6 +199,10 @@ class Submission(models.Model):
     class Meta:
         app_label = "submissions"
         ordering = ["-submission_date"]
+        permissions = [
+            ("take_edadmin_actions", "Take editorial admin actions"),
+            ("view_edadmin_info", "View editorial admin information"),
+        ]
 
     def save(self, *args, **kwargs):
         """Prefill some fields before saving."""
@@ -537,6 +544,18 @@ class Submission(models.Model):
         return None
 
 
+# The next two models are for optimization of django guardian object-level permissions
+# using direct foreign keys instead of generic ones
+# (see https://django-guardian.readthedocs.io/en/stable/userguide/performance.html)
+
+class SubmissionUserObjectPermission(UserObjectPermissionBase):
+    content_object = models.ForeignKey(Submission, on_delete=models.CASCADE)
+
+class SubmissionGroupObjectPermission(GroupObjectPermissionBase):
+    content_object = models.ForeignKey(Submission, on_delete=models.CASCADE)
+
+
+
 class SubmissionEvent(SubmissionRelatedObjectMixin, TimeStampedModel):
     """Private message directly related to a Submission.
 
diff --git a/scipost_django/submissions/urls.py b/scipost_django/submissions/urls.py
index 4594db967..7e6c93078 100644
--- a/scipost_django/submissions/urls.py
+++ b/scipost_django/submissions/urls.py
@@ -17,8 +17,7 @@ urlpatterns = [
         views.SubmissionAutocompleteView.as_view(),
         name="submission-autocomplete",
     ),
-    # Submissions
-    path("", views.SubmissionListView.as_view(), name="submissions"),
+    # Information
     path(
         "author_guidelines",
         TemplateView.as_view(template_name="submissions/author_guidelines.html"),
@@ -34,6 +33,8 @@ urlpatterns = [
         TemplateView.as_view(template_name="submissions/referee_guidelines.html"),
         name="referee_guidelines",
     ),
+    # Submissions
+    path("", views.SubmissionListView.as_view(), name="submissions"),
     path(
         "<identifier_wo_vn_nr:identifier_wo_vn_nr>/",
         views.submission_detail_wo_vn_nr,
-- 
GitLab