@@ -157,8 +157,13 @@ Authors
{{ f_author.institution|se_can_see_pii:article }}
{% if article.correspondence_author and f_author.author == article.correspondence_author %}
{% else %} {% endif %}
- Edit
+ {% if can_see_pii_tag %}
+ Edit
+ {% else %}
+ Author data cannot be edited
+ {% endif %}
+
{{ entry.email_subject }}
{{ entry.date|date:"Y-m-d H:i:s" }}
- {% if entry.actor %}{{ entry.actor.full_name }}{% else %}Janeway System{% endif %}
+ {% if entry.actor %}{{ entry.actor.full_name|se_can_see_pii:article }}{% else %}Janeway System{% endif %}
{{ entry.get_level_display|capfirst }}
{% if settings.ENABLE_ENHANCED_MAILGUN_FEATURES %}
{% if entry.is_email %}
From ec9d3532c7432c55d0da1f3387385504d732d867 Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 17:44:19 +0100
Subject: [PATCH 25/49] Moves logic to logic.py module
---
src/security/logic.py | 25 +++++++++++++++++++
src/security/templatetags/securitytags.py | 29 ++---------------------
2 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/src/security/logic.py b/src/security/logic.py
index 2afb4b21d7..8d40e873d9 100755
--- a/src/security/logic.py
+++ b/src/security/logic.py
@@ -5,6 +5,7 @@
from production import models as production_models
from proofing import models as proofing_models
from submission import models as submission_models
+from utils import setting_handler
def can_edit_file(request, user, file_object, article):
@@ -125,3 +126,27 @@ def is_data_figure_file(file_object, article_object):
# deny access to all others
return False
+
+
+def can_see_pii(request, article):
+ # Before doing anything, check the setting is enabled:
+ se_pii_filter_enabled = setting_handler.get_setting(
+ setting_group_name='permission',
+ setting_name='se_pii_filter',
+ journal=article.journal,
+ ).processed_value
+
+ if not se_pii_filter_enabled:
+ return False
+
+ # Check if the user is an SE and return an anonymised value.
+ # If the user is not a section editor we assume they have permission
+ # to view the actual value.
+ stages = [
+ submission_models.STAGE_UNASSIGNED,
+ submission_models.STAGE_UNDER_REVIEW,
+ submission_models.STAGE_UNDER_REVISION,
+ ]
+ if request.user in article.section_editors() and article.stage in stages:
+ return True
+ return False
diff --git a/src/security/templatetags/securitytags.py b/src/security/templatetags/securitytags.py
index 1515b72d3a..0e945112ed 100755
--- a/src/security/templatetags/securitytags.py
+++ b/src/security/templatetags/securitytags.py
@@ -3,7 +3,6 @@
from core.middleware import GlobalRequestMiddleware
from security import logic
from submission import models
-from utils import setting_handler
register = template.Library()
@@ -93,35 +92,11 @@ def is_preprint_editor(context):
return request.user.is_preprint_editor(request)
-def can_see_pii(request, article):
- # Before doing anything, check the setting is enabled:
- se_pii_filter_enabled = setting_handler.get_setting(
- setting_group_name='permission',
- setting_name='se_pii_filter',
- journal=article.journal,
- ).processed_value
-
- if not se_pii_filter_enabled:
- return False
-
- # Check if the user is an SE and return an anonymised value.
- # If the user is not a section editor we assume they have permission
- # to view the actual value.
- stages = [
- models.STAGE_UNASSIGNED,
- models.STAGE_UNDER_REVIEW,
- models.STAGE_UNDER_REVISION,
- ]
- if request.user in article.section_editors() and article.stage in stages:
- return True
- return False
-
-
@register.filter
def se_can_see_pii(value, article):
request = GlobalRequestMiddleware.get_current_request()
- if can_see_pii(request, article):
+ if logic.can_see_pii(request, article):
return 'Value Anonymised'
else:
return value
@@ -131,7 +106,7 @@ def se_can_see_pii(value, article):
def can_see_pii_tag(context, article):
request = context.get('request')
- if can_see_pii(request, article):
+ if logic.can_see_pii(request, article):
return False
else:
return True
From 9ac9dffca52f98215571a52246fdb5270c5b2781 Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:32:01 +0100
Subject: [PATCH 26/49] Adds new security decorator for SE PII check
---
src/security/decorators.py | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/security/decorators.py b/src/security/decorators.py
index 5112e58de5..50abb9040a 100755
--- a/src/security/decorators.py
+++ b/src/security/decorators.py
@@ -18,7 +18,13 @@
from submission import models
from copyediting import models as copyediting_models
from proofing import models as proofing_models
-from security.logic import can_edit_file, can_view_file_history, can_view_file, is_data_figure_file
+from security.logic import (
+ can_edit_file,
+ can_see_pii,
+ can_view_file,
+ can_view_file_history,
+ is_data_figure_file,
+)
from utils import setting_handler
from utils.logger import get_logger
from repository import models as preprint_models
@@ -288,6 +294,25 @@ def wrapper(request, *args, **kwargs):
return wrapper
+def editor_user_required_and_can_see_pii(func):
+ """Extends editor_user_required to check if SE can see PII"""
+ @editor_user_required
+ def can_see_pii_decorator(request, *args, **kwargs):
+ article_id = kwargs.get('article_id')
+ article = get_object_or_404(models.Article, pk=article_id)
+ if (
+ request.user in article.section_editors()
+ and not can_see_pii(request, article)
+ ):
+ deny_access(
+ request,
+ "You cannot access this page yet, because it could reveal"
+ " personally identifable information"
+ )
+ return func(request, *args, **kwargs)
+ return can_see_pii_decorator
+
+
def any_editor_user_required(func):
"""Checks if the user is any type of editor
or otherwise is a staff member.
From d665aa91ded1d7848e6180e37d0efc373347bbf6 Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:32:24 +0100
Subject: [PATCH 27/49] Use new decorator on document manager
---
src/journal/views.py | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/journal/views.py b/src/journal/views.py
index f595254f72..841417c70a 100755
--- a/src/journal/views.py
+++ b/src/journal/views.py
@@ -43,10 +43,20 @@
from metrics.logic import store_article_access
from review import forms as review_forms, models as review_models
from submission import encoding
-from security.decorators import article_stage_accepted_or_later_required, \
- article_stage_accepted_or_later_or_staff_required, article_exists, file_user_required, has_request, has_journal, \
- file_history_user_required, file_edit_user_required, production_user_or_editor_required, \
- editor_user_required, keyword_page_enabled
+from security.decorators import (
+ article_exists,
+ article_stage_accepted_or_later_required,
+ article_stage_accepted_or_later_or_staff_required,
+ editor_user_required,
+ editor_user_required_and_can_see_pii,
+ file_edit_user_required,
+ file_history_user_required,
+ file_user_required,
+ has_journal,
+ has_request,
+ keyword_page_enabled,
+ production_user_or_editor_required,
+)
from submission import models as submission_models
from utils import models as utils_models, shared, setting_handler
from utils.logger import get_logger
@@ -2432,7 +2442,7 @@ def texture_edit(request, file_id):
return render(request, template, context)
-@editor_user_required
+@editor_user_required_and_can_see_pii
def document_management(request, article_id):
document_article = get_object_or_404(
submission_models.Article,
From abb2f28a5a7a523aeb163986cbe7ee3587409ee4 Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:32:46 +0100
Subject: [PATCH 28/49] Adds security decorator test case
---
src/security/test_security.py | 32 +++++++++++++++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 2510b9a905..6a9c31ed15 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -29,7 +29,7 @@
from press import models as press_models
from utils.install import update_xsl_files, update_settings
from utils import setting_handler
-from utils.testing import helpers
+from utils.testing import context_managers, helpers
class TestSecurity(TestCase):
@@ -3104,6 +3104,29 @@ def test_section_editor_cant_access_random_article(self):
with self.assertRaises(PermissionDenied):
decorated_func(request, **kwargs)
+ def test_section_editor_cant_access_view_because_of_pii(self):
+ review_models.EditorAssignment.objects.get_or_create(
+ article=self.article_in_review,
+ editor=self.section_editor,
+ editor_type='section-editor',
+ notified=True,
+ )
+ func = Mock()
+
+ with context_managers.janeway_setting_override(
+ "permission", "se_pii_filter", self.journal_one, True,
+ ):
+ decorated_func = decorators.editor_user_required_and_can_see_pii(func)
+ kwargs = {'article_id': self.article_in_review.pk}
+
+ request = self.prepare_request_with_user(self.section_editor, self.journal_one)
+
+ with self.assertRaises(
+ PermissionDenied,
+ "SE PII filter not leading to PermissionDenied"
+ ):
+ decorated_func(request, **kwargs)
+
def test_article_stage_review_required_with_review_article(self):
func = Mock()
decorated_func = decorators.article_stage_review_required(func)
@@ -4150,6 +4173,13 @@ def setUpTestData(self):
self.third_file.save()
+ self.article_in_review = submission_models.Article.objects.create(
+ owner=self.regular_user, title="A Test Article in review",
+ abstract="An abstract",
+ stage=submission_models.STAGE_UNDER_REVIEW,
+ journal_id=self.journal_one.id,
+ )
+
self.article_in_production = submission_models.Article(owner=self.regular_user, title="A Test Article",
abstract="An abstract",
stage=submission_models.STAGE_TYPESETTING,
From e5b2a1c5ff3256b5b298c173c8992f376c442d6e Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:33:50 +0100
Subject: [PATCH 29/49] Adds utility for cleanly overriding janeway setting in
test
---
src/utils/testing/context_managers.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 src/utils/testing/context_managers.py
diff --git a/src/utils/testing/context_managers.py b/src/utils/testing/context_managers.py
new file mode 100644
index 0000000000..569bcdb7a2
--- /dev/null
+++ b/src/utils/testing/context_managers.py
@@ -0,0 +1,27 @@
+from contextlib import contextmanager
+
+from utils import setting_handler
+
+
+@contextmanager
+def janeway_setting_override(setting_group, setting_name, journal=None, value=None):
+ prev_value = setting_handler.get_setting(
+ setting_group,
+ setting_name,
+ journal=journal,
+ ).value
+ try:
+ setting_handler.save_setting(
+ setting_group,
+ setting_name,
+ journal=journal,
+ value=value,
+ )
+ yield
+ finally:
+ setting_handler.save_setting(
+ setting_group,
+ setting_name,
+ journal=journal,
+ value=prev_value,
+ )
From b98714c9bef7cf90d8808708155a667711f4c933 Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:34:18 +0100
Subject: [PATCH 30/49] Fix wrong logic on new SE PII checks
---
src/security/logic.py | 7 ++++---
src/security/templatetags/securitytags.py | 8 ++++----
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/security/logic.py b/src/security/logic.py
index 8d40e873d9..e64819d754 100755
--- a/src/security/logic.py
+++ b/src/security/logic.py
@@ -136,8 +136,9 @@ def can_see_pii(request, article):
journal=article.journal,
).processed_value
+ # early return if filter not enabled
if not se_pii_filter_enabled:
- return False
+ return True
# Check if the user is an SE and return an anonymised value.
# If the user is not a section editor we assume they have permission
@@ -148,5 +149,5 @@ def can_see_pii(request, article):
submission_models.STAGE_UNDER_REVISION,
]
if request.user in article.section_editors() and article.stage in stages:
- return True
- return False
+ return False
+ return True
diff --git a/src/security/templatetags/securitytags.py b/src/security/templatetags/securitytags.py
index 0e945112ed..95755c4c6d 100755
--- a/src/security/templatetags/securitytags.py
+++ b/src/security/templatetags/securitytags.py
@@ -97,9 +97,9 @@ def se_can_see_pii(value, article):
request = GlobalRequestMiddleware.get_current_request()
if logic.can_see_pii(request, article):
- return 'Value Anonymised'
- else:
return value
+ else:
+ return 'Value Anonymised'
@register.simple_tag(takes_context=True)
@@ -107,9 +107,9 @@ def can_see_pii_tag(context, article):
request = context.get('request')
if logic.can_see_pii(request, article):
- return False
- else:
return True
+ else:
+ return False
From d035bd4de707f3c7b8150f379c3bb185ceacaffc Mon Sep 17 00:00:00 2001
From: Mauro MSL
Date: Tue, 24 Sep 2024 18:38:50 +0100
Subject: [PATCH 31/49] Fix test
---
src/security/test_security.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 6a9c31ed15..60b9c030f9 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -3123,7 +3123,7 @@ def test_section_editor_cant_access_view_because_of_pii(self):
with self.assertRaises(
PermissionDenied,
- "SE PII filter not leading to PermissionDenied"
+ msg="SE PII filter not leading to PermissionDenied",
):
decorated_func(request, **kwargs)
From 134004c1c699f8c85d50444bcae24d2dfadf5385 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 20:32:40 +0100
Subject: [PATCH 32/49] #4417 include STAGE_ASSIGNED in list of stages
---
src/security/logic.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/security/logic.py b/src/security/logic.py
index e64819d754..277b7c4299 100755
--- a/src/security/logic.py
+++ b/src/security/logic.py
@@ -145,6 +145,7 @@ def can_see_pii(request, article):
# to view the actual value.
stages = [
submission_models.STAGE_UNASSIGNED,
+ submission_models.STAGE_ASSIGNED,
submission_models.STAGE_UNDER_REVIEW,
submission_models.STAGE_UNDER_REVISION,
]
From e12635c645e41fc10900cc5eefd2bc35d2ec57f2 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 20:57:54 +0100
Subject: [PATCH 33/49] #4417 include securitytags as a builtin tag set
---
src/core/janeway_global_settings.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/core/janeway_global_settings.py b/src/core/janeway_global_settings.py
index 6d265b70f8..28258c1d43 100755
--- a/src/core/janeway_global_settings.py
+++ b/src/core/janeway_global_settings.py
@@ -169,6 +169,7 @@
],
'builtins': [
'core.templatetags.fqdn',
+ 'security.templatetags.securitytags',
'django.templatetags.i18n',
]
},
From f48bbc34c55aa17bde8529ce6ac1f32317cea268 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 20:58:46 +0100
Subject: [PATCH 34/49] #4417 add se screening filter to draft templates
---
src/utils/install/journal_defaults.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json
index 6cf53a0542..cd9d899656 100644
--- a/src/utils/install/journal_defaults.json
+++ b/src/utils/install/journal_defaults.json
@@ -275,7 +275,7 @@
"type": "rich-text"
},
"value": {
- "default": "Dear {{ article.correspondence_author.full_name }}, We are requesting that you make some changes to your paper. You can access your revision here: {{ do_revisions_url }} Your revisions are due on {{ revision.date_due }}. Regards, {{ request.user.signature|safe }}"
+ "default": "Dear {{ article.correspondence_author.full_name|se_can_see_pii:article }},
We are requesting that you make some changes to your paper.
You can access your revision here: {{ do_revisions_url }}
Your revisions are due on {{ revision.date_due }}.
Regards,
{{ request.user.signature|safe }}"
},
"editable_by": [
"editor",
@@ -446,7 +446,7 @@
"type": "rich-text"
},
"value": {
- "default": "Dear {{ article.correspondence_author.full_name }}, We are happy to inform you that your article, \"{{ article.safe_title }}\", has been accepted in {{ article.journal.name }}. Thank you for submitting such a strong manuscript.
Peer review reports and feedback on the manuscript can be viewed at: {{ review_url }}
We will be back in touch soon to discuss the next stages of production.
Regards,
{{ request.user.signature|safe }}
"
+ "default": "Dear {{ article.correspondence_author.full_name|se_can_see_pii:article }}, We are happy to inform you that your article, \"{{ article.safe_title }}\", has been accepted in {{ article.journal.name }}. Thank you for submitting such a strong manuscript.
Peer review reports and feedback on the manuscript can be viewed at: {{ review_url }}
We will be back in touch soon to discuss the next stages of production.
Regards,
{{ request.user.signature|safe }}
"
},
"editable_by": [
"editor",
@@ -465,7 +465,7 @@
"type": "rich-text"
},
"value": {
- "default": "Dear {{ article.correspondence_author.full_name }}, We are sorry to inform you that \"{{ article.safe_title }}\" has been declined in {{ article.journal.name }}. You can view your reviews and feedback on the manuscript at: {{ review_url }} Regards,
{{ request.user.signature|safe }}
"
+ "default": "Dear {{ article.correspondence_author.full_name|se_can_see_pii:article }}, We are sorry to inform you that \"{{ article.safe_title }}\" has been declined in {{ article.journal.name }}. You can view your reviews and feedback on the manuscript at: {{ review_url }} Regards,
{{ request.user.signature|safe }}
"
},
"editable_by": [
"editor",
From 578ed7b2e0bc04e0b8acf298e6605863d6a22330 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 21:29:59 +0100
Subject: [PATCH 35/49] #4417 adds filter to revision complete updates pii
filter description
---
src/utils/install/journal_defaults.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json
index cd9d899656..689c448d9a 100644
--- a/src/utils/install/journal_defaults.json
+++ b/src/utils/install/journal_defaults.json
@@ -4870,7 +4870,7 @@
"type": "rich-text"
},
"value": {
- "default": "{{ revision.article.correspondence_author.full_name }} has successfully completed the assigned revision task for article #{{ revision.article.pk }} - \"{{ revision.article.title }}.\"
Please take a moment to review the revisions at {{ url }}.
"
+ "default": "{{ revision.article.correspondence_author.full_name|se_can_see_pii:revision.article }} has successfully completed the assigned revision task for article #{{ revision.article.pk }} - \"{{ revision.article.title }}.\"
Please take a moment to review the revisions at {{ url }}.
"
},
"editable_by": [
"editor",
@@ -5123,7 +5123,7 @@
"name": "permission"
},
"setting": {
- "description": "When this setting is enabled assigned section editors will not be able to see PII data about authors until a decision for an article has been made.",
+ "description": "When this setting is enabled assigned section editors will not be able to see PII data about authors until a decision for an article has been made. The general setting Reply-To Address should be set in order for this setting to work correctly.",
"is_translatable": false,
"name": "se_pii_filter",
"pretty_name": "Section Editor Personally Identifiable Information Filter",
From a4118deaa4dd4acb8d2bf2b12b1a8e18deb1e7ec Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 21:30:27 +0100
Subject: [PATCH 36/49] #4417 add custom_reply_to to send_revisions_complete
---
src/utils/transactional_emails.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/utils/transactional_emails.py b/src/utils/transactional_emails.py
index ddc44664d7..cddb57e3b7 100644
--- a/src/utils/transactional_emails.py
+++ b/src/utils/transactional_emails.py
@@ -594,6 +594,21 @@ def send_revisions_complete(**kwargs):
},
)
)
+
+ # Get custom reply-to if se pii is enabled.
+ se_pii_filter_enabled = request.journal.get_setting(
+ group_name='permission',
+ setting_name='se_pii_filter',
+ )
+ custom_reply_to = None
+ if se_pii_filter_enabled:
+ custom_reply_to_value = request.journal.get_setting(
+ group_name='general',
+ setting_name='replyto_address',
+ )
+ if custom_reply_to_value:
+ custom_reply_to = custom_reply_to_value
+
notify_helpers.send_email_with_body_from_setting_template(
request=request,
template='revisions_complete_editor_notification',
@@ -609,7 +624,8 @@ def send_revisions_complete(**kwargs):
'action_text': description,
'types': 'Revisions Complete',
'target': article,
- }
+ },
+ custom_reply_to=custom_reply_to,
)
notify_helpers.send_slack(request, description, ['slack_editors'])
From e13759255a1b83b24395b0beddfb5445a6af2a01 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Tue, 24 Sep 2024 21:50:18 +0100
Subject: [PATCH 37/49] #4417 addd migration to add pii filter to templates
---
src/core/migrations/0098_add_pii_filter.py | 45 ++++++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 src/core/migrations/0098_add_pii_filter.py
diff --git a/src/core/migrations/0098_add_pii_filter.py b/src/core/migrations/0098_add_pii_filter.py
new file mode 100644
index 0000000000..207b751cb4
--- /dev/null
+++ b/src/core/migrations/0098_add_pii_filter.py
@@ -0,0 +1,45 @@
+# Generated by Django 4.2 on 2024-09-24 20:32
+
+from django.db import migrations
+from utils import migration_utils
+
+updates = [
+ {
+ 'setting_names': ['review_decision_accept', 'request_revisions', 'review_decision_decline'],
+ 'replacements': [(
+ '{{ article.correspondence_author.full_name }}',
+ '{{ article.correspondence_author.full_name|se_can_see_pii:article }}'
+ )]
+ },
+ {
+ 'setting_names': ['revisions_complete_editor_notification'],
+ 'replacements': [(
+ '{{ revision.article.correspondence_author.full_name }}',
+ '{{ revision.article.correspondence_author.full_name|se_can_see_pii:revision.article }}'
+ )]
+ },
+]
+
+
+def replace_values(apps, schema_editor):
+ for update in updates:
+ for setting_name in update.get('setting_names'):
+ migration_utils.replace_strings_in_setting_values(
+ apps=apps,
+ setting_name=setting_name,
+ group_name='email',
+ replacements=update.get('replacements'),
+ )
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('core', '0097_merge_20240819_1617'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ replace_values,
+ reverse_code=migrations.RunPython.noop,
+ )
+ ]
From ae03ef9894d18649a3b38c14b2314a2d5a3a0729 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 13:10:07 +0100
Subject: [PATCH 38/49] #4417 adds a test for edited views.
---
src/security/decorators.py | 1 -
src/security/test_security.py | 112 +++++++++++++++++++++++++++++++---
2 files changed, 103 insertions(+), 10 deletions(-)
diff --git a/src/security/decorators.py b/src/security/decorators.py
index 50abb9040a..67f94460c2 100755
--- a/src/security/decorators.py
+++ b/src/security/decorators.py
@@ -275,7 +275,6 @@ def editor_user_required(func):
@base_check_required
def wrapper(request, *args, **kwargs):
-
article_id = kwargs.get('article_id', None)
if request.user.is_editor(request) or request.user.is_staff or request.user.is_journal_manager(request.journal):
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 60b9c030f9..4e809a4230 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -3105,12 +3105,6 @@ def test_section_editor_cant_access_random_article(self):
decorated_func(request, **kwargs)
def test_section_editor_cant_access_view_because_of_pii(self):
- review_models.EditorAssignment.objects.get_or_create(
- article=self.article_in_review,
- editor=self.section_editor,
- editor_type='section-editor',
- notified=True,
- )
func = Mock()
with context_managers.janeway_setting_override(
@@ -3990,10 +3984,77 @@ def test_user_has_completed_review_for_article_granted(self):
"permission denied.",
)
+ # PII Tests
+ def test_section_editor_cannot_see_pii_when_enabled(self):
+ """
+ Test that the section editor cannot see PII and it is anonymized in
+ the response.
+ """
+ with context_managers.janeway_setting_override(
+ "permission", "se_pii_filter", self.article_in_review.journal, True,
+ ):
+ self.client.force_login(self.section_editor)
+ article_views = [
+ 'manage_article_log',
+ 'edit_metadata',
+ 'review_unassigned_article',
+ 'review_in_review',
+ ]
+ general_views = [
+ 'core_dashboard',
+ 'review_home',
+ 'core_active_submissions',
+ ]
+ list_of_pii_strings = self.get_pii_strings_for_article(
+ self.article_in_review,
+ )
+
+ for view_name in general_views:
+ response = self.client.get(
+ reverse(
+ view_name,
+ ),
+ SERVER_NAME=self.article_in_review.journal.domain,
+ )
+ found_strings = [
+ string in response.content.decode('utf-8') for string in list_of_pii_strings
+ ]
+ self.assertFalse(any(found_strings))
+ self.assertContains(
+ response,
+ 'Value Anonymised'
+ )
+
+ for view_name in article_views:
+ response = self.client.get(
+ reverse(
+ view_name,
+ kwargs={
+ 'article_id': self.article_in_review.pk,
+ }
+ ),
+ SERVER_NAME=self.article_in_review.journal.domain,
+ )
+ found_strings = [
+ string in response.content.decode('utf-8') for string in
+ list_of_pii_strings
+ ]
+ self.assertFalse(any(found_strings))
+ self.assertContains(
+ response,
+ self.article_in_review.pk,
+ )
+
# General helper functions
@staticmethod
- def create_user(username, roles=None, journal=None):
+ def create_user(
+ username,
+ roles=None,
+ journal=None,
+ first_name='',
+ last_name='',
+ ):
"""
Creates a user with the specified permissions.
:return: a user with the specified permissions
@@ -4003,6 +4064,7 @@ def create_user(username, roles=None, journal=None):
username,
roles=roles,
journal=journal,
+ **{'first_name': first_name, 'last_name': last_name}
)
@staticmethod
@@ -4030,6 +4092,18 @@ def create_journals():
return journal_one, journal_two
+ @staticmethod
+ def get_pii_strings_for_article(article):
+ pii_strings = []
+ for fa in article.frozen_authors():
+ pii_strings.append(fa.first_name)
+ pii_strings.append(fa.last_name)
+ pii_strings.append(fa.email)
+ pii_strings.append(article.correspondence_author.first_name)
+ pii_strings.append(article.correspondence_author.last_name)
+ pii_strings.append(article.correspondence_author.email)
+ return [string for string in pii_strings if string]
+
@classmethod
def setUpTestData(self):
"""
@@ -4063,7 +4137,13 @@ def setUpTestData(self):
self.editor.is_active = True
self.editor.save()
- self.author = self.create_user("authoruser@martineve.com", ["author"], journal=self.journal_one)
+ self.author = self.create_user(
+ "authoruser@martineve.com",
+ ["author"],
+ journal=self.journal_one,
+ first_name="Martin",
+ last_name="Eve",
+ )
self.author.is_active = True
self.author.save()
@@ -4178,8 +4258,22 @@ def setUpTestData(self):
abstract="An abstract",
stage=submission_models.STAGE_UNDER_REVIEW,
journal_id=self.journal_one.id,
+ correspondence_author=self.repo_manager,
+ )
+ self.article_in_review.authors.add(
+ self.author,
+ )
+ self.article_in_review.snapshot_authors()
+ review_models.ReviewRound.objects.get_or_create(
+ article=self.article_in_review,
+ round_number=1,
+ )
+ review_models.EditorAssignment.objects.get_or_create(
+ article=self.article_in_review,
+ editor=self.section_editor,
+ editor_type='section-editor',
+ notified=True,
)
-
self.article_in_production = submission_models.Article(owner=self.regular_user, title="A Test Article",
abstract="An abstract",
stage=submission_models.STAGE_TYPESETTING,
From b88d0b4f1a5a35475dc0480b4a70e04365d88062 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 15:25:39 +0100
Subject: [PATCH 39/49] #4417 change setting description
---
src/utils/install/journal_defaults.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json
index 689c448d9a..4601c628ea 100644
--- a/src/utils/install/journal_defaults.json
+++ b/src/utils/install/journal_defaults.json
@@ -5123,7 +5123,7 @@
"name": "permission"
},
"setting": {
- "description": "When this setting is enabled assigned section editors will not be able to see PII data about authors until a decision for an article has been made. The general setting Reply-To Address should be set in order for this setting to work correctly.",
+ "description": "When this setting is enabled assigned section editors will not be able to see personally identifiable information about authors until a decision for an article has been made. The general setting Reply-To Address should be set in order for this setting to work correctly.",
"is_translatable": false,
"name": "se_pii_filter",
"pretty_name": "Section Editor Personally Identifiable Information Filter",
From e77d7a7c0fa5662f865c4bdb6a4b2bffcfe81654 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 15:26:07 +0100
Subject: [PATCH 40/49] #4417 fix typo
---
src/security/decorators.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/security/decorators.py b/src/security/decorators.py
index 67f94460c2..7b53afee47 100755
--- a/src/security/decorators.py
+++ b/src/security/decorators.py
@@ -306,7 +306,7 @@ def can_see_pii_decorator(request, *args, **kwargs):
deny_access(
request,
"You cannot access this page yet, because it could reveal"
- " personally identifable information"
+ " personally identifiable information."
)
return func(request, *args, **kwargs)
return can_see_pii_decorator
From fafc5ce4d4f980d4133127ac95dcba9750c7c36d Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 15:34:07 +0100
Subject: [PATCH 41/49] #4417 changes wording and marks for translation
---
src/security/templatetags/securitytags.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/security/templatetags/securitytags.py b/src/security/templatetags/securitytags.py
index 95755c4c6d..52599950c2 100755
--- a/src/security/templatetags/securitytags.py
+++ b/src/security/templatetags/securitytags.py
@@ -2,7 +2,7 @@
from core.middleware import GlobalRequestMiddleware
from security import logic
-from submission import models
+from django.utils.translation import gettext_lazy as _
register = template.Library()
@@ -99,7 +99,7 @@ def se_can_see_pii(value, article):
if logic.can_see_pii(request, article):
return value
else:
- return 'Value Anonymised'
+ return _('[Anonymised data]')
@register.simple_tag(takes_context=True)
From dc710dd5e510164c354c0cc41a7c593f7a750b08 Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 16:38:33 +0100
Subject: [PATCH 42/49] #4417 updates test.
---
src/security/test_security.py | 41 +++++++++++++++++++++--------------
1 file changed, 25 insertions(+), 16 deletions(-)
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 4e809a4230..51ed0cfa52 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -3999,11 +3999,14 @@ def test_section_editor_cannot_see_pii_when_enabled(self):
'edit_metadata',
'review_unassigned_article',
'review_in_review',
+ 'review_decision',
+ 'decision_helper',
]
general_views = [
'core_dashboard',
'review_home',
'core_active_submissions',
+ 'review_unassigned',
]
list_of_pii_strings = self.get_pii_strings_for_article(
self.article_in_review,
@@ -4017,21 +4020,21 @@ def test_section_editor_cannot_see_pii_when_enabled(self):
SERVER_NAME=self.article_in_review.journal.domain,
)
found_strings = [
- string in response.content.decode('utf-8') for string in list_of_pii_strings
+ string in response.content.decode('utf-8') for string in
+ list_of_pii_strings
]
self.assertFalse(any(found_strings))
- self.assertContains(
- response,
- 'Value Anonymised'
- )
for view_name in article_views:
+ kwargs = {
+ 'article_id': self.article_in_review.pk,
+ }
+ if view_name == 'review_decision':
+ kwargs['decision'] = 'accept'
response = self.client.get(
reverse(
view_name,
- kwargs={
- 'article_id': self.article_in_review.pk,
- }
+ kwargs=kwargs,
),
SERVER_NAME=self.article_in_review.journal.domain,
)
@@ -4040,10 +4043,6 @@ def test_section_editor_cannot_see_pii_when_enabled(self):
list_of_pii_strings
]
self.assertFalse(any(found_strings))
- self.assertContains(
- response,
- self.article_in_review.pk,
- )
# General helper functions
@@ -4099,9 +4098,15 @@ def get_pii_strings_for_article(article):
pii_strings.append(fa.first_name)
pii_strings.append(fa.last_name)
pii_strings.append(fa.email)
+ pii_strings.append(fa.orcid if fa.orcid else '')
+ pii_strings.append(fa.institution)
pii_strings.append(article.correspondence_author.first_name)
pii_strings.append(article.correspondence_author.last_name)
pii_strings.append(article.correspondence_author.email)
+ pii_strings.append(
+ article.correspondence_author.orcid if article.correspondence_author.orcid else ''
+ )
+ pii_strings.append(article.correspondence_author.institution)
return [string for string in pii_strings if string]
@classmethod
@@ -4138,11 +4143,11 @@ def setUpTestData(self):
self.editor.save()
self.author = self.create_user(
- "authoruser@martineve.com",
+ "b.torres@voyager.com",
["author"],
journal=self.journal_one,
- first_name="Martin",
- last_name="Eve",
+ first_name="Belanna",
+ last_name="Torres",
)
self.author.is_active = True
self.author.save()
@@ -4210,7 +4215,11 @@ def setUpTestData(self):
self.staff_member.is_staff = True
self.staff_member.save()
- self.repo_manager = self.create_user("repomanager@janeway.systems")
+ self.repo_manager = self.create_user(
+ "repomanager@janeway.systems",
+ first_name='Tom',
+ last_name='Paris',
+ )
self.repo_manager.is_active = True
self.repo_manager.save()
From e4390934b60cc99709d3ad105fb18d46d2aa90df Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 16:38:49 +0100
Subject: [PATCH 43/49] #4417 Add security decorator to file_history view.
---
src/journal/views.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/journal/views.py b/src/journal/views.py
index 841417c70a..69e518d153 100755
--- a/src/journal/views.py
+++ b/src/journal/views.py
@@ -848,6 +848,7 @@ def submit_files_info(request, article_id, file_id):
@login_required
@file_history_user_required
+@editor_user_required_and_can_see_pii
def file_history(request, article_id, file_id):
""" Renders a template to show the history of a file.
From 9a400a813a5cbf1a78af4d0f48eaa8784b77a94d Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Wed, 25 Sep 2024 16:39:13 +0100
Subject: [PATCH 44/49] #4417 fix visible pii.
---
.../admin/elements/core/submission_list_element.html | 2 +-
src/templates/admin/journal/file_history.html | 4 ++--
src/templates/admin/review/decision.html | 4 ++--
src/templates/admin/review/decision_helper.html | 2 +-
src/templates/admin/review/unassigned.html | 2 +-
src/templates/admin/review/unassigned_article.html | 7 ++++++-
6 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/templates/admin/elements/core/submission_list_element.html b/src/templates/admin/elements/core/submission_list_element.html
index d0ce776d0d..3a45164277 100644
--- a/src/templates/admin/elements/core/submission_list_element.html
+++ b/src/templates/admin/elements/core/submission_list_element.html
@@ -2,7 +2,7 @@
-
{{ article.pk }} - {{ article.title|truncatechars_html:200|safe }} ({{ article.correspondence_author.last_name }})
+
{{ article.pk }} - {{ article.title|truncatechars_html:200|safe }} ({{ article.correspondence_author.last_name|se_can_see_pii:article }})
Authors: {{ article.author_list|se_can_see_pii:article }}
{% for editor in article.editors %}{% if forloop.first %}Editors: {% endif %}{{ editor.editor.full_name }}
diff --git a/src/templates/admin/journal/file_history.html b/src/templates/admin/journal/file_history.html
index e5c1286b88..ded8a42d65 100644
--- a/src/templates/admin/journal/file_history.html
+++ b/src/templates/admin/journal/file_history.html
@@ -67,11 +67,11 @@
{% empty %}
- No older versions of this file
+ No older versions of this file
{% endfor %}
-
+
{{ file.pk }}
{{ file.label }}
{{ file }}
diff --git a/src/templates/admin/review/decision.html b/src/templates/admin/review/decision.html
index 22e2c39cab..af68a07cc9 100644
--- a/src/templates/admin/review/decision.html
+++ b/src/templates/admin/review/decision.html
@@ -3,7 +3,7 @@
{% block title %}{{ decision|capfirst }} Article{% endblock title %}
{% block title-section %}{{ decision|capfirst }} Article{% endblock %}
-{% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name }} / {{ article.safe_title }}{% endblock %}
+{% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name|se_can_see_pii:article }} / {{ article.safe_title }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
@@ -66,7 +66,7 @@ {{ review.reviewer.full_name }} (Round {{ review.review_round.round_number }
You can provide some information to the authors below:
-
To {{ article.correspondence_author.full_name }}
+ To {{ article.correspondence_author.full_name|se_can_see_pii:article }}
From {{ request.user.full_name }}
{% if article.stage == 'Unassigned' %}
diff --git a/src/templates/admin/review/decision_helper.html b/src/templates/admin/review/decision_helper.html
index 1a35663b83..02150b0c1c 100644
--- a/src/templates/admin/review/decision_helper.html
+++ b/src/templates/admin/review/decision_helper.html
@@ -5,7 +5,7 @@
{% block title %}#{{ article.pk }} Decision Helper{% endblock title %}
{% block title-section %}#{{ article.pk }} Decision Helper{% endblock %}
-{% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name }} / {{ article.safe_title }}{% endblock %}
+{% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name|se_can_see_pii:article }} / {{ article.safe_title }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
diff --git a/src/templates/admin/review/unassigned.html b/src/templates/admin/review/unassigned.html
index 668ac63a69..c0f62af28b 100644
--- a/src/templates/admin/review/unassigned.html
+++ b/src/templates/admin/review/unassigned.html
@@ -33,7 +33,7 @@
Unassigned Articles
{{ article.pk }}
{{ article.safe_title }}
{{ article.date_submitted }}
-
{{ article.correspondence_author.full_name }}
+
{{ article.correspondence_author.full_name|se_can_see_pii:article }}
{{ article.section.name }}
View
diff --git a/src/templates/admin/review/unassigned_article.html b/src/templates/admin/review/unassigned_article.html
index 30a8b0812f..1d4f06a4ef 100644
--- a/src/templates/admin/review/unassigned_article.html
+++ b/src/templates/admin/review/unassigned_article.html
@@ -1,5 +1,6 @@
{% extends "admin/core/base.html" %}
{% load static roles i18n securitytags %}
+{% can_see_pii_tag article as can_see_pii %}
{% block title %}Unassigned {{ article.title }}{% endblock %}
{% block title-section %}Unassigned{% endblock %}
@@ -29,7 +30,7 @@ Summary of Article
{{ article.section.name }}
- {{ article.owner.full_name }}
+ {{ article.owner.full_name|se_can_see_pii:article }}
{{ article.license.short_name }}
{{ article.get_language_display }}
@@ -149,7 +150,9 @@ Files
Uploaded
Download
Replace
+ {% if can_see_pii %}
File History
+ {% endif %}
{% if journal_settings.crosscheck.enable %}
Similarity Check {% endif %}
@@ -166,9 +169,11 @@ Files
+ {% if can_see_pii %}
+ {% endif %}
{% if journal_settings.crosscheck.enable %}
{% if not article.ithenticate_id %}
Date: Wed, 25 Sep 2024 16:49:19 +0100
Subject: [PATCH 45/49] #4417 include department in checks.
---
src/security/test_security.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 51ed0cfa52..85f8a2c9cf 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -4100,9 +4100,11 @@ def get_pii_strings_for_article(article):
pii_strings.append(fa.email)
pii_strings.append(fa.orcid if fa.orcid else '')
pii_strings.append(fa.institution)
+ pii_strings.append(fa.department)
pii_strings.append(article.correspondence_author.first_name)
pii_strings.append(article.correspondence_author.last_name)
pii_strings.append(article.correspondence_author.email)
+ pii_strings.append(article.correspondence_author.department)
pii_strings.append(
article.correspondence_author.orcid if article.correspondence_author.orcid else ''
)
From 2e7b1b1f6f8a3b3b95ced1d66258e6ed8a28e30c Mon Sep 17 00:00:00 2001
From: Andy Byers
Date: Thu, 26 Sep 2024 12:38:24 +0100
Subject: [PATCH 46/49] #4417 adds additional PII to test.
---
src/security/test_security.py | 39 +++++++++++++++++++++++++++++++++--
1 file changed, 37 insertions(+), 2 deletions(-)
diff --git a/src/security/test_security.py b/src/security/test_security.py
index 85f8a2c9cf..c1ab2e72b2 100644
--- a/src/security/test_security.py
+++ b/src/security/test_security.py
@@ -4053,17 +4053,36 @@ def create_user(
journal=None,
first_name='',
last_name='',
+ institution='',
+ department='',
+ orcid='',
+ country_name='',
+ country_code='',
):
"""
Creates a user with the specified permissions.
:return: a user with the specified permissions
"""
# For consistency, outsourced to newer testing helpers
+ country_obj = None
+ if country_code and country_name:
+ country_obj, c = core_models.Country.objects.get_or_create(
+ code=country_code,
+ name=country_name,
+ )
+
return helpers.create_user(
username,
roles=roles,
journal=journal,
- **{'first_name': first_name, 'last_name': last_name}
+ **{
+ 'first_name': first_name,
+ 'last_name': last_name,
+ 'institution': institution,
+ 'department': department,
+ 'orcid': orcid,
+ 'country': country_obj,
+ }
)
@staticmethod
@@ -4101,6 +4120,7 @@ def get_pii_strings_for_article(article):
pii_strings.append(fa.orcid if fa.orcid else '')
pii_strings.append(fa.institution)
pii_strings.append(fa.department)
+ pii_strings.append(fa.country)
pii_strings.append(article.correspondence_author.first_name)
pii_strings.append(article.correspondence_author.last_name)
pii_strings.append(article.correspondence_author.email)
@@ -4109,6 +4129,7 @@ def get_pii_strings_for_article(article):
article.correspondence_author.orcid if article.correspondence_author.orcid else ''
)
pii_strings.append(article.correspondence_author.institution)
+ pii_strings.append(article.correspondence_author.country)
return [string for string in pii_strings if string]
@classmethod
@@ -4122,7 +4143,16 @@ def setUpTestData(self):
"production", "copyeditor", "typesetter",
"proofing-manager", "section-editor"])
- self.regular_user = self.create_user("regularuser@martineve.com")
+ self.regular_user = self.create_user(
+ "redshirt@voyager.com",
+ first_name='Tim',
+ last_name='Redshirt',
+ institution='Starfleet Ops',
+ department='Canon Fodder',
+ orcid='1234-1234-1234-0000',
+ country_name='United States of America',
+ country_code='US',
+ )
self.regular_user.is_active = True
self.regular_user.save()
@@ -4150,6 +4180,11 @@ def setUpTestData(self):
journal=self.journal_one,
first_name="Belanna",
last_name="Torres",
+ institution='Starfleet',
+ department='Engineering',
+ orcid='0000-1234-1234-1234',
+ country_code='FR',
+ country_name='France',
)
self.author.is_active = True
self.author.save()
From ecc05d4bbe78384e8bd85afc1192ef0ab222bb93 Mon Sep 17 00:00:00 2001
From: Joseph Muller
Date: Mon, 24 Jun 2024 10:42:51 +0100
Subject: [PATCH 47/49] Fix bug that double-encoded URL query #4291 #3899
---
src/utils/tests.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/src/utils/tests.py b/src/utils/tests.py
index 5ce3bc1616..3ce0f2cab1 100644
--- a/src/utils/tests.py
+++ b/src/utils/tests.py
@@ -7,6 +7,7 @@
import os
from django.apps import apps
+from django.http import QueryDict
from django.test import TestCase, override_settings
from django.utils import timezone, translation
from django.core import mail
@@ -1210,6 +1211,8 @@ def test_update_default_setting_values(self):
new_value,
saved_value,
)
+
+
class TestORCiDRecord(TestCase):
all_fields = {'orcid-identifier': {'uri': 'http://sandbox.orcid.org/0000-0000-0000-0000', 'path': '0000-0000-0000-0000', 'host': 'sandbox.orcid.org'}, 'preferences': {'locale': 'EN'}, 'history': {'creation-method': 'DIRECT', 'completion-date': None, 'submission-date': {'value': 1716899022299}, 'last-modified-date': {'value': 1717012729950}, 'claimed': True, 'source': None, 'deactivation-date': None, 'verified-email': True, 'verified-primary-email': True}, 'person': {'last-modified-date': {'value': 1717012710380}, 'name': {'created-date': {'value': 1716899022606}, 'last-modified-date': {'value': 1716931428927}, 'given-names': {'value': 'cdleschol'}, 'family-name': {'value': 'arship'}, 'credit-name': None, 'source': None, 'visibility': 'PUBLIC', 'path': '0000-0000-0000-0000'}, 'other-names': {'last-modified-date': None, 'other-name': [], 'path': '/0000-0000-0000-0000/other-names'}, 'biography': None, 'researcher-urls': {'last-modified-date': None, 'researcher-url': [], 'path': '/0000-0000-0000-0000/researcher-urls'}, 'emails': {'last-modified-date': {'value': 1717012710380}, 'email': [{'created-date': {'value': 1716899022599}, 'last-modified-date': {'value': 1717012710380}, 'source': {'source-orcid': {'uri': 'http://sandbox.orcid.org/0000-0000-0000-0000', 'path': '0000-0000-0000-0000', 'host': 'sandbox.orcid.org'}, 'source-client-id': None, 'source-name': {'value': 'cdleschol arship'}}, 'email': 'cdleschol@mailinator.com', 'path': None, 'visibility': 'PUBLIC', 'verified': True, 'primary': True, 'put-code': None}], 'path': '/0000-0000-0000-0000/email'}, 'addresses': {'last-modified-date': {'value': 1716931402191}, 'address': [{'created-date': {'value': 1716931402191}, 'last-modified-date': {'value': 1716931402191}, 'source': {'source-orcid': {'uri': 'http://sandbox.orcid.org/0000-0000-0000-0000', 'path': '0000-0000-0000-0000', 'host': 'sandbox.orcid.org'}, 'source-client-id': None, 'source-name': {'value': 'cdleschol arship'}}, 'country': {'value': 'US'}, 'visibility': 'PUBLIC', 'path': '/0000-0000-0000-0000/address/7884', 'put-code': 7884, 'display-index': 1}], 'path': '/0000-0000-0000-0000/address'}, 'keywords': {'last-modified-date': None, 'keyword': [], 'path': '/0000-0000-0000-0000/keywords'}, 'external-identifiers': {'last-modified-date': None, 'external-identifier': [], 'path': '/0000-0000-0000-0000/external-identifiers'}, 'path': '/0000-0000-0000-0000/person'}, 'activities-summary': {'last-modified-date': {'value': 1716931455651}, 'educations': {'last-modified-date': None, 'education-summary': [], 'path': '/0000-0000-0000-0000/educations'}, 'employments': {'last-modified-date': {'value': 1716931455651}, 'employment-summary': [{'created-date': {'value': 1716931455651}, 'last-modified-date': {'value': 1716931455651}, 'source': {'source-orcid': {'uri': 'http://sandbox.orcid.org/0000-0000-0000-0000', 'path': '0000-0000-0000-0000', 'host': 'sandbox.orcid.org'}, 'source-client-id': None, 'source-name': {'value': 'cdleschol arship'}}, 'department-name': None, 'role-title': None, 'start-date': None, 'end-date': None, 'organization': {'name': 'California Digital Library', 'address': {'city': 'Oakland', 'region': 'California', 'country': 'US'}, 'disambiguated-organization': {'disambiguated-organization-identifier': 'https://ror.org/03yrm5c26', 'disambiguation-source': 'ROR'}}, 'visibility': 'PUBLIC', 'put-code': 66225, 'path': '/0000-0000-0000-0000/employment/66225'}], 'path': '/0000-0000-0000-0000/employments'}, 'fundings': {'last-modified-date': None, 'group': [], 'path': '/0000-0000-0000-0000/fundings'}, 'peer-reviews': {'last-modified-date': None, 'group': [], 'path': '/0000-0000-0000-0000/peer-reviews'}, 'works': {'last-modified-date': None, 'group': [], 'path': '/0000-0000-0000-0000/works'}, 'path': '/0000-0000-0000-0000/activities'}, 'path': '/0000-0000-0000-0000'}
@@ -1243,3 +1246,60 @@ def test_redirect_uri(self):
repo = helpers.create_repository(press, [], [])
self.assertEqual(build_redirect_uri(repo), "http://localhost/login/orcid/?state=login")
self.assertEqual(build_redirect_uri(repo, action="register"), "http://localhost/login/orcid/?state=register")
+
+
+class URLLogicTests(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ # The unicode string of a 'next' URL
+ cls.next_url = '/target/page/?a=b&x=y'
+ # The above string url-encoded with safe='/'
+ cls.next_url_encoded = '/target/page/%3Fa%3Db%26x%3Dy'
+ # The above string prepended with 'next='
+ cls.next_url_query_string = 'next=/target/page/%3Fa%3Db%26x%3Dy'
+ # The core_login url with encoded next url
+ cls.core_login_with_next = '/login/?next=/target/page/%3Fa%3Db%26x%3Dy'
+
+ def test_build_url_query_as_querydict(self):
+ querydict = QueryDict('a=b&a=c', mutable=True)
+ querydict.update({'next': self.next_url})
+ url = logic.build_url(
+ 'example.org',
+ scheme='https',
+ path='/path/',
+ query=querydict,
+ )
+ self.assertEqual(
+ url,
+ 'https://example.org/path/?a=b&a=c&next=/target/page/%3Fa%3Db%26x%3Dy',
+ )
+
+ def test_build_url_query_as_plain_dict(self):
+ plain_dict = {
+ 'a': 'b',
+ 'next': self.next_url,
+ }
+ url = logic.build_url(
+ 'example.org',
+ scheme='https',
+ path='/path/',
+ query=plain_dict,
+ )
+ self.assertEqual(
+ url,
+ 'https://example.org/path/?a=b&next=/target/page/%3Fa%3Db%26x%3Dy',
+ )
+
+ def test_build_url_query_as_string(self):
+ query_string = f'a=b&a=c&{ self.next_url_query_string }'
+ url = logic.build_url(
+ 'example.org',
+ scheme='https',
+ path='/path/',
+ query=query_string,
+ )
+ self.assertEqual(
+ url,
+ 'https://example.org/path/?a=b&a=c&next=/target/page/%3Fa%3Db%26x%3Dy',
+ )
From 5c1d67cf7cc9bd8f7b285dd99e3c1a7446963aa9 Mon Sep 17 00:00:00 2001
From: Joseph Muller
Date: Mon, 24 Jun 2024 10:51:15 +0100
Subject: [PATCH 48/49] Fix bug that garbled redirect query string #4042 #3899
---
src/security/decorators.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/security/decorators.py b/src/security/decorators.py
index 7b53afee47..22bebb9f44 100755
--- a/src/security/decorators.py
+++ b/src/security/decorators.py
@@ -52,7 +52,7 @@ def base_check(request, login_redirect=False):
params = urlencode({"next": f"{request.path}?{request_params}"})
return redirect('{0}?{1}'.format(reverse('core_login'), params))
elif isinstance(login_redirect, str):
- params = urlencode({"next": redirect})
+ params = urlencode({"next": login_redirect})
return redirect('{0}?{1}'.format(reverse('core_login'), params))
else:
return False
From b15a1a94996252e0551f73a7dd9aa6224962f3a1 Mon Sep 17 00:00:00 2001
From: Joseph Muller
Date: Thu, 8 Aug 2024 12:37:38 +0100
Subject: [PATCH 49/49] WIP
---
src/core/locales/cy/LC_MESSAGES/django.po | 2 +-
src/core/locales/de/LC_MESSAGES/django.po | 2 +-
src/core/locales/en_us/LC_MESSAGES/django.po | 2 +-
src/core/locales/es/LC_MESSAGES/django.po | 4 +-
src/core/locales/fr/LC_MESSAGES/django.po | 2 +-
src/core/locales/it/LC_MESSAGES/django.po | 2 +-
src/core/locales/nl/LC_MESSAGES/django.po | 2 +-
src/core/logic.py | 139 ++++---
src/core/models.py | 10 +-
src/core/templatetags/next_url.py | 46 +++
src/core/tests/test_logic.py | 78 +++-
src/core/tests/test_views.py | 369 ++++++++++++++++++
src/core/views.py | 120 ++++--
src/journal/models.py | 1 +
src/press/forms.py | 4 -
src/press/models.py | 2 -
src/security/decorators.py | 5 +-
src/templates/admin/journal/submissions.html | 8 +-
src/templates/admin/press/edit_press.html | 2 -
.../core/accounts/activate_account.html | 14 +-
.../core/accounts/orcid_registration.html | 47 ++-
src/themes/OLH/templates/core/base.html | 12 +-
src/themes/OLH/templates/core/login.html | 25 +-
src/themes/OLH/templates/core/nav.html | 13 +-
.../templates/elements/journal_footer.html | 8 +-
.../templates/journal/become_reviewer.html | 6 +-
.../OLH/templates/journal/submissions.html | 5 +-
.../OLH/templates/press/core/login.html | 6 +-
.../press/elements/press_footer.html | 10 +-
src/themes/OLH/templates/press/nav.html | 5 +-
.../core/accounts/activate_account.html | 14 +-
.../core/accounts/orcid_registration.html | 57 +--
src/themes/clean/templates/core/login.html | 27 +-
src/themes/clean/templates/core/nav.html | 12 +-
.../templates/elements/journal_footer.html | 5 +-
.../templates/elements/press_footer.html | 7 +-
.../templates/journal/become_reviewer.html | 8 +-
.../clean/templates/journal/submissions.html | 4 +-
src/themes/clean/templates/press/nav.html | 16 +-
.../core/accounts/activate_account.html | 11 +-
.../core/accounts/orcid_registration.html | 59 +++
src/themes/material/templates/core/login.html | 28 +-
src/themes/material/templates/core/nav.html | 25 +-
.../templates/journal/become_reviewer.html | 8 +-
.../templates/journal/submissions.html | 10 +-
src/themes/material/templates/press/nav.html | 24 +-
.../material/templates/repository/nav.html | 17 +-
.../templates/repository/preprint.html | 3 +-
src/utils/install/journal_defaults.json | 4 +-
src/utils/logic.py | 3 +-
src/utils/orcid.py | 3 +-
src/utils/testing/helpers.py | 50 +++
state-documentation.md | 3 +
state-management-during-authentication.md | 21 +
54 files changed, 1130 insertions(+), 240 deletions(-)
create mode 100644 src/core/templatetags/next_url.py
create mode 100644 src/core/tests/test_views.py
create mode 100644 src/themes/material/templates/core/accounts/orcid_registration.html
create mode 100644 state-documentation.md
create mode 100644 state-management-during-authentication.md
diff --git a/src/core/locales/cy/LC_MESSAGES/django.po b/src/core/locales/cy/LC_MESSAGES/django.po
index 22cf3df04d..ef306ac62d 100644
--- a/src/core/locales/cy/LC_MESSAGES/django.po
+++ b/src/core/locales/cy/LC_MESSAGES/django.po
@@ -393,7 +393,7 @@ msgstr ""
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow theinstructions in the email "
"that has been sent to you."
msgstr ""
diff --git a/src/core/locales/de/LC_MESSAGES/django.po b/src/core/locales/de/LC_MESSAGES/django.po
index 60ab4e0975..cfa63b87da 100644
--- a/src/core/locales/de/LC_MESSAGES/django.po
+++ b/src/core/locales/de/LC_MESSAGES/django.po
@@ -382,7 +382,7 @@ msgstr "Falls Ihr Konto gefunden wurde, wurde Ihnen eine E-Mail geschickt."
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow the instructions in the email "
"that has been sent to you."
msgstr ""
"Ihr Konto wurde angelegt. Bitte folgen Sie den Anweisungen aus der E-Mail, "
diff --git a/src/core/locales/en_us/LC_MESSAGES/django.po b/src/core/locales/en_us/LC_MESSAGES/django.po
index d2dc27c0d1..6540e1bcad 100644
--- a/src/core/locales/en_us/LC_MESSAGES/django.po
+++ b/src/core/locales/en_us/LC_MESSAGES/django.po
@@ -373,7 +373,7 @@ msgstr ""
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow the instructions in the email "
"that has been sent to you."
msgstr ""
diff --git a/src/core/locales/es/LC_MESSAGES/django.po b/src/core/locales/es/LC_MESSAGES/django.po
index 36fbb19112..3751295200 100644
--- a/src/core/locales/es/LC_MESSAGES/django.po
+++ b/src/core/locales/es/LC_MESSAGES/django.po
@@ -4440,7 +4440,9 @@ msgid "\n"
msgstr ""
#: src/core/views.py:355
-msgid "Your account has been created, please follow theinstructions in the email that has been sent to you."
+msgid ""
+"Your account has been created. Please follow the instructions in the email "
+"that has been sent to you."
msgstr ""
#: src/core/views.py:400
diff --git a/src/core/locales/fr/LC_MESSAGES/django.po b/src/core/locales/fr/LC_MESSAGES/django.po
index bbc2dbec6c..a23f4a0fc4 100755
--- a/src/core/locales/fr/LC_MESSAGES/django.po
+++ b/src/core/locales/fr/LC_MESSAGES/django.po
@@ -394,7 +394,7 @@ msgstr ""
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow the instructions in the email "
"that has been sent to you."
msgstr ""
diff --git a/src/core/locales/it/LC_MESSAGES/django.po b/src/core/locales/it/LC_MESSAGES/django.po
index 760b34a480..1bc84d7af4 100644
--- a/src/core/locales/it/LC_MESSAGES/django.po
+++ b/src/core/locales/it/LC_MESSAGES/django.po
@@ -395,7 +395,7 @@ msgstr ""
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow the instructions in the email "
"that has been sent to you."
msgstr ""
diff --git a/src/core/locales/nl/LC_MESSAGES/django.po b/src/core/locales/nl/LC_MESSAGES/django.po
index 5991d5d07b..c975f2bd16 100644
--- a/src/core/locales/nl/LC_MESSAGES/django.po
+++ b/src/core/locales/nl/LC_MESSAGES/django.po
@@ -389,7 +389,7 @@ msgstr ""
#: src/core/views.py:355
msgid ""
-"Your account has been created, please follow theinstructions in the email "
+"Your account has been created. Please follow the instructions in the email "
"that has been sent to you."
msgstr ""
diff --git a/src/core/logic.py b/src/core/logic.py
index 98268f3ae4..83aaabb2ad 100755
--- a/src/core/logic.py
+++ b/src/core/logic.py
@@ -11,13 +11,14 @@
import operator
import re
from functools import reduce
+from urllib.parse import unquote, urlparse
from django.conf import settings
from django.contrib.auth import logout
from django.contrib import messages
from django.template.loader import get_template
from django.db.models import Q
-from django.http import JsonResponse
+from django.http import JsonResponse, QueryDict
from django.forms.models import model_to_dict
from django.shortcuts import reverse
from django.utils import timezone
@@ -35,81 +36,117 @@
logger = get_logger(__name__)
+def get_raw_next_url(next_url, request):
+ """
+ Get the next_url passed in or the 'next' on the request, as raw unicode.
+ :param next_url: an optional string with the path and query parts of
+ a destination URL -- overrides any 'next' in request data
+ :param request: HttpRequest, optionally containing 'next' in GET or POST
+ """
+ if not next_url:
+ next_url = request.GET.get('next', '') or request.POST.get('next', '')
+ return unquote(next_url)
+
+
+def reverse_with_next(url_name, request, next_url='', args=None, kwargs=None):
+ """
+ Reverse a URL but keep the 'next' parameter that exists on the request
+ or that the caller wants to introduce.
+ The value of 'next' on the request or 'next_url' can be in raw unicode or
+ it can have been percent-encoded one time.
+ :param request: HttpRequest, optionally containing 'next' in GET or POST
+ :param next_url: an optional string with the path and query parts of
+ a destination URL -- overrides any 'next' in request data
+ :param args: args to pass to django.shortcuts.reverse, if no kwargs
+ :param kwargs: kwargs to pass to django.shortcuts.reverse, if no args
+ """
+ # reverse can only accept either args or kwargs
+ if args:
+ reversed_url = reverse(url_name, args=args)
+ elif kwargs:
+ reversed_url = reverse(url_name, kwargs=kwargs)
+ else:
+ reversed_url = reverse(url_name)
+
+ raw_next_url = get_raw_next_url(next_url, request)
+
+ if not raw_next_url:
+ return reversed_url
+
+ if reversed_url == raw_next_url:
+ # Avoid circular next URLs
+ return reversed_url
+
+ # Parse the reversed URL string enough to safely update the query parameters.
+ # Then re-encode them into a query string and generate the final URL.
+ parsed_url = urlparse(reversed_url) # ParseResult
+ parsed_query = QueryDict(parsed_url.query, mutable=True) # mutable QueryDict
+ parsed_query.update({'next': raw_next_url})
+ # We treat / as safe to match the default behavior
+ # of the |urlencode template filter,
+ # which is where many next URLs are created
+ new_query_string = parsed_query.urlencode(safe="/") # Full percent-encoded query string
+ return parsed_url._replace(query=new_query_string).geturl()
+
+
def send_reset_token(request, reset_token):
core_reset_password_url = request.site_type.site_url(
reverse(
'core_reset_password',
kwargs={'token': reset_token.token},
- )
+ ),
+ query={'next': get_raw_next_url('', request)},
)
context = {
'reset_token': reset_token,
'core_reset_password_url': core_reset_password_url,
}
log_dict = {'level': 'Info', 'types': 'Reset Token', 'target': None}
- if not request.journal:
- message = render_template.get_message_content(
- request,
- context,
- request.press.password_reset_text,
- template_is_setting=True,
- )
- else:
- message = render_template.get_message_content(
- request,
- context,
- 'password_reset',
- )
-
- subject = 'subject_password_reset'
-
- notify_helpers.send_email_with_body_from_user(
+ notify_helpers.send_email_with_body_from_setting_template(
request,
- subject,
+ 'password_reset',
+ 'subject_password_reset',
reset_token.account.email,
- message,
+ context,
log_dict=log_dict,
)
-def send_confirmation_link(request, new_user):
- core_confirm_account_url = request.site_type.site_url(
+def get_confirm_account_url(request, user, next_url=''):
+ return request.site_type.site_url(
reverse(
'core_confirm_account',
- kwargs={'token': new_user.confirmation_code},
- )
+ kwargs={'token': user.confirmation_code},
+ ),
+ query={'next': get_raw_next_url(next_url, request)},
)
+
+
+def send_confirmation_link(request, new_user):
+ core_confirm_account_url = get_confirm_account_url(request, new_user)
+ if request.journal:
+ site_name = request.journal.name
+ elif request.repository:
+ site_name = request.repository.name
+ else:
+ site_name = request.press.name
context = {
'user': new_user,
+ 'site_name': site_name,
'core_confirm_account_url': core_confirm_account_url,
}
- if not request.journal:
- message = render_template.get_message_content(
- request,
- context,
- request.press.registration_text,
- template_is_setting=True,
- )
- else:
- message = render_template.get_message_content(
- request,
- context,
- 'new_user_registration',
- )
-
- subject = 'subject_new_user_registration'
-
notify_helpers.send_slack(
request,
'New registration: {0}'.format(new_user.full_name()),
['slack_admins'],
)
log_dict = {'level': 'Info', 'types': 'Account Confirmation', 'target': None}
- notify_helpers.send_email_with_body_from_user(
+ notify_helpers.send_email_with_body_from_setting_template(
request,
- subject,
+ 'new_user_registration',
+ 'subject_new_user_registration',
new_user.email,
- message,
+ context,
log_dict=log_dict,
)
@@ -644,20 +681,18 @@ def handle_article_thumb_image_file(uploaded_file, article, request):
article.save()
-def handle_email_change(request, email_address):
+def handle_email_change(request, email_address, next_url=''):
request.user.email = email_address
request.user.is_active = False
request.user.confirmation_code = uuid.uuid4()
request.user.clean()
request.user.save()
- core_confirm_account_url = request.site_type.site_url(
- reverse(
- 'core_confirm_account',
- kwargs={'token': request.user.confirmation_code},
- )
+ core_confirm_account_url = get_confirm_account_url(
+ request,
+ request.user,
+ next_url=next_url,
)
-
context = {
'user': request.user,
'core_confirm_account_url': core_confirm_account_url,
@@ -868,7 +903,7 @@ def check_for_bad_login_attempts(request):
time = timezone.now() - timedelta(minutes=10)
attempts = models.LoginAttempt.objects.filter(user_agent=user_agent, ip_address=ip_address, timestamp__gte=time)
- print(time, attempts.count())
+ logger.debug(f'Bad login attempt {attempts.count()+1} at {time}')
return attempts.count()
diff --git a/src/core/models.py b/src/core/models.py
index f3ba5e03ae..9cec7725f4 100644
--- a/src/core/models.py
+++ b/src/core/models.py
@@ -255,6 +255,7 @@ class Account(AbstractBaseUser, PermissionsMixin):
validators=[plain_text_validator],
)
+ # activation_code is deprecated
activation_code = models.CharField(max_length=100, null=True, blank=True)
salutation = models.CharField(
max_length=10,
@@ -294,7 +295,14 @@ class Account(AbstractBaseUser, PermissionsMixin):
profile_image = models.ImageField(upload_to=profile_images_upload_path, null=True, blank=True, storage=fs, verbose_name=("Profile Image"))
email_sent = models.DateTimeField(blank=True, null=True)
date_confirmed = models.DateTimeField(blank=True, null=True)
- confirmation_code = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Confirmation Code"))
+ confirmation_code = models.CharField(
+ max_length=200,
+ blank=True,
+ null=True,
+ verbose_name=_("Confirmation Code"),
+ help_text='A UUID created upon registration and retrieved '
+ 'for authentication during account activation',
+ )
signature = JanewayBleachField(
blank=True,
verbose_name=_("Signature"),
diff --git a/src/core/templatetags/next_url.py b/src/core/templatetags/next_url.py
new file mode 100644
index 0000000000..ed92e27008
--- /dev/null
+++ b/src/core/templatetags/next_url.py
@@ -0,0 +1,46 @@
+from django import template
+from django.urls import reverse
+from core.logic import reverse_with_next
+from urllib.parse import quote
+
+
+register = template.Library()
+
+@register.simple_tag(takes_context=True)
+def url_with_next(context, url_name, next_url_name='', *args, **kwargs):
+ """
+ A template tag to use instead of 'url' when you want
+ the reversed URL to include the same 'next' parameter that
+ already exists in the GET or POST data of the request,
+ or you want to introduce a new next url by Django URL name.
+ """
+ if next_url_name:
+ next_url = reverse(next_url_name)
+ else:
+ next_url = ''
+ request = context.get('request')
+ return reverse_with_next(
+ url_name,
+ request,
+ next_url=next_url,
+ *args,
+ **kwargs,
+ )
+
+
+@register.simple_tag(takes_context=True)
+def url_with_return(context, url_name, *args, **kwargs):
+ """
+ A template tag to use instead of 'url' when you want
+ the reversed URL to include a new 'next' parameter that
+ contains the full path of the current request.
+ """
+ request = context.get('request')
+ next_url = quote(request.get_full_path())
+ return reverse_with_next(
+ url_name,
+ request,
+ next_url=next_url,
+ *args,
+ **kwargs,
+ )
diff --git a/src/core/tests/test_logic.py b/src/core/tests/test_logic.py
index 591837b27a..2504f4ed4c 100644
--- a/src/core/tests/test_logic.py
+++ b/src/core/tests/test_logic.py
@@ -1,18 +1,37 @@
+__copyright__ = "Copyright 2024 Birkbeck, University of London"
+__author__ = "Open Library of Humanities"
+__license__ = "AGPL v3"
+__maintainer__ = "Open Library of Humanities"
+
+import uuid
+from mock import patch
+
from django.test import TestCase
from core import logic
-from core.models import SettingGroup
from utils.testing import helpers
class TestLogic(TestCase):
- def setUp(self):
- self.press = helpers.create_press()
- self.press.save()
- self.journal_one, self.journal_two = helpers.create_journals()
- self.request = helpers.Request()
- self.request.press = self.press
- self.request.journal = self.journal_one
- self.request.site_type = self.journal_one
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.press = helpers.create_press()
+ cls.press.save()
+ cls.journal_one, cls.journal_two = helpers.create_journals()
+ cls.request = helpers.Request()
+ cls.request.press = cls.press
+ cls.request.journal = cls.journal_one
+ cls.request.site_type = cls.journal_one
+ cls.request.GET = {}
+ cls.request.POST = {}
+ cls.inactive_user = helpers.create_user('zlwdi6frbtlh4gditdir@example.org')
+ cls.inactive_user.is_active = False
+ cls.inactive_user.confirmation_code = '8bd3cdc9-1c3c-4ec9-99bc-9ea0b86a3c55'
+ cls.inactive_user.clean()
+ cls.inactive_user.save()
+
+ # The result of passing a URL through the |urlencode template filter
+ cls.next_url_encoded = '/target/page/%3Fq%3Da'
def test_render_nested_settings(self):
expected_rendered_setting = "For help with Janeway, contact --No support email set-- .
"
@@ -23,3 +42,44 @@ def test_render_nested_settings(self):
nested_settings=[('support_email','general')],
)
self.assertEqual(expected_rendered_setting, rendered_setting)
+
+ @patch('core.logic.reverse')
+ def test_reverse_with_next_in_get_request(self, mock_reverse):
+ mock_reverse.return_value = '/my/path/?my=params'
+ self.request.GET = {'next': self.next_url_encoded}
+ reversed_url = logic.reverse_with_next('/test/', self.request)
+ self.assertIn(self.next_url_encoded, reversed_url)
+
+ @patch('core.logic.reverse')
+ def test_reverse_with_next_in_post_request(self, mock_reverse):
+ mock_reverse.return_value = '/my/path/?my=params'
+ self.request.POST = {'next': self.next_url_encoded}
+ reversed_url = logic.reverse_with_next('/test/', self.request)
+ self.assertIn(self.next_url_encoded, reversed_url)
+
+ @patch('core.logic.reverse')
+ def test_reverse_with_next_in_kwarg(self, mock_reverse):
+ mock_reverse.return_value = '/my/path/?my=params'
+ reversed_url = logic.reverse_with_next(
+ '/test/',
+ self.request,
+ next_url=self.next_url_encoded,
+ )
+ self.assertIn(self.next_url_encoded, reversed_url)
+
+ @patch('core.logic.reverse')
+ def test_reverse_with_next_no_next(self, mock_reverse):
+ mock_reverse.return_value = '/my/url/?my=params'
+ reversed_url = logic.reverse_with_next('/test/', self.request)
+ self.assertEqual(mock_reverse.return_value, reversed_url)
+
+ def test_get_confirm_account_url(self):
+ url = logic.get_confirm_account_url(
+ self.request,
+ self.inactive_user,
+ next_url=self.next_url_encoded,
+ )
+ self.assertIn(
+ f'/register/step/2/8bd3cdc9-1c3c-4ec9-99bc-9ea0b86a3c55/?next={ self.next_url_encoded }',
+ url,
+ )
diff --git a/src/core/tests/test_views.py b/src/core/tests/test_views.py
new file mode 100644
index 0000000000..9b0db8a928
--- /dev/null
+++ b/src/core/tests/test_views.py
@@ -0,0 +1,369 @@
+__copyright__ = "Copyright 2024 Birkbeck, University of London"
+__author__ = "Open Library of Humanities"
+__license__ = "AGPL v3"
+__maintainer__ = "Open Library of Humanities"
+
+from mock import patch
+from uuid import uuid4
+
+from django.test import Client, TestCase, override_settings
+
+from utils.testing import helpers
+
+from core import models as core_models
+
+
+class NextURLTests(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.press = helpers.create_press()
+ cls.journal_one, cls.journal_two = helpers.create_journals()
+ cls.theme_dirs = helpers.get_theme_dirs()
+ helpers.create_roles(['author'])
+ cls.user_email = 'sukv8golcvwervs0y7e5@example.org'
+ cls.user_password = 'xUMXW1oXn2l8L26Kixi2'
+ cls.user = core_models.Account.objects.create_user(
+ cls.user_email,
+ password=cls.user_password,
+ )
+ cls.user.is_active = True
+ cls.user_orcid = 'https://orcid.org/0000-0001-2345-6789'
+ cls.user.orcid = cls.user_orcid
+ cls.orcid_token_str = uuid4()
+ cls.orcid_token = core_models.OrcidToken.objects.create(
+ token=cls.orcid_token_str,
+ orcid=cls.user_orcid,
+ )
+ cls.reset_token_str = uuid4()
+ cls.reset_token = core_models.PasswordResetToken.objects.create(
+ account=cls.user,
+ token=cls.reset_token_str,
+ )
+ cls.user.save()
+
+ # The raw unicode string of a 'next' URL
+ cls.next_url_raw = '/target/page/?a=b&x=y'
+
+ # The unicode string url-encoded with safe='/'
+ cls.next_url_encoded = '/target/page/%3Fa%3Db%26x%3Dy'
+
+ # The unicode string url-encoded with safe=''
+ cls.next_url_encoded_no_safe = '%2Ftarget%2Fpage%2F%3Fa%3Db%26x%3Dy'
+
+ # next_url_encoded with its 'next' key
+ cls.next_url_query_string = 'next=/target/page/%3Fa%3Db%26x%3Dy'
+
+ # The core_login url with encoded next url
+ cls.core_login_with_next = '/login/?next=/target/page/%3Fa%3Db%26x%3Dy'
+
+ def setUp(self):
+ self.client = Client()
+
+
+class UserLoginTests(NextURLTests):
+
+ def test_is_authenticated_redirects_to_next(self):
+ self.client.login(username=self.user_email, password=self.user_password)
+ data = {
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/', data, follow=True)
+ self.assertIn((self.next_url_raw, 302), response.redirect_chain)
+
+ @patch('core.views.authenticate')
+ def test_login_success_redirects_to_next(self, authenticate):
+ authenticate.return_value = self.user
+ data = {
+ 'user_name': self.user_email,
+ 'user_pass': self.user_password,
+ 'next': self.next_url_raw,
+ }
+ response = self.client.post('/login/', data, follow=True)
+ self.assertIn((self.next_url_raw, 302), response.redirect_chain)
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ @override_settings(ENABLE_OIDC=True)
+ def test_oidc_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ response = self.client.get('/login/', data)
+ self.assertIn(
+ f'/oidc/authenticate/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ @override_settings(ENABLE_ORCID=True)
+ def test_orcid_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ response = self.client.get('/login/', data)
+ self.assertIn(
+ f'/login/orcid/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ def test_forgot_password_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ response = self.client.get('/login/', data)
+ self.assertIn(
+ f'/reset/step/1/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ def test_register_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ response = self.client.get('/login/', data)
+ self.assertIn(
+ f'/register/step/1/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+
+class UserLoginOrcidTests(NextURLTests):
+
+ @override_settings(ENABLE_ORCID=False)
+ def test_orcid_disabled_redirects_with_next(self):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data, follow=True)
+ self.assertIn(self.next_url_encoded, response.redirect_chain[0][0])
+
+ @override_settings(ENABLE_ORCID=True)
+ def test_no_orcid_code_redirects_with_next(self):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data)
+ self.assertIn(self.next_url_encoded_no_safe, response.url)
+
+ @patch('core.views.orcid.retrieve_tokens')
+ @override_settings(ENABLE_ORCID=True)
+ def test_orcid_id_account_found_redirects_to_next(
+ self,
+ retrieve_tokens,
+ ):
+ retrieve_tokens.return_value = self.user_orcid
+ data = {
+ 'code': '12345',
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data, follow=True)
+ self.assertIn((self.next_url_raw, 302), response.redirect_chain)
+
+ @patch('core.views.orcid.get_orcid_record_details')
+ @patch('core.views.orcid.retrieve_tokens')
+ @override_settings(ENABLE_ORCID=True)
+ def test_orcid_id_no_account_matching_email_redirects_to_next(
+ self,
+ retrieve_tokens,
+ orcid_details,
+ ):
+ # Change ORCID so it doesn't work
+ retrieve_tokens.return_value = 'https://orcid.org/0000-0001-2312-3123'
+
+ # Return an email that will work
+ orcid_details.return_value = {'emails': [self.user_email]}
+
+ data = {
+ 'code': '12345',
+ 'state': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data, follow=True)
+ self.assertIn((self.next_url_raw, 302), response.redirect_chain)
+
+ @patch('core.views.orcid.get_orcid_record_details')
+ @patch('core.views.orcid.retrieve_tokens')
+ @override_settings(ENABLE_ORCID=True)
+ def test_orcid_id_no_account_no_matching_email_redirects_to_next(
+ self,
+ retrieve_tokens,
+ orcid_details,
+ ):
+ # Change ORCID so it doesn't work
+ retrieve_tokens.return_value = 'https://orcid.org/0000-0001-2312-3123'
+
+ orcid_details.return_value = {'emails': []}
+ data = {
+ 'code': '12345',
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data, follow=True)
+ self.assertIn(
+ self.next_url_query_string,
+ response.redirect_chain[0][0],
+ )
+
+ @patch('core.views.orcid.retrieve_tokens')
+ @override_settings(ENABLE_ORCID=True)
+ def test_no_orcid_id_retrieved_redirects_with_next(self, retrieve_tokens):
+ retrieve_tokens.return_value = None
+ data = {
+ 'code': '12345',
+ 'next': self.next_url_raw,
+ }
+ response = self.client.get('/login/orcid/', data, follow=True)
+ self.assertIn((self.core_login_with_next, 302), response.redirect_chain)
+
+
+class GetResetTokenTests(NextURLTests):
+
+ @patch('core.views.logic.start_reset_process')
+ def test_start_reset_redirects_with_next(self, _start_reset):
+ data = {
+ 'email_address': self.user_email,
+ 'next': self.next_url_raw,
+ }
+ response = self.client.post('/reset/step/1/', data, follow=True)
+ self.assertIn((self.core_login_with_next, 302), response.redirect_chain)
+
+
+class ResetPasswordTests(NextURLTests):
+
+ @patch('core.views.logic.password_policy_check')
+ def test_reset_password_form_valid_redirects_with_next(self, password_check):
+ password_check.return_value = None
+ data = {
+ 'password_1': 'qsX1roLama3ADotEopfq',
+ 'password_2': 'qsX1roLama3ADotEopfq',
+ 'next': self.next_url_raw,
+ }
+ reset_step_2_path = f'/reset/step/2/{self.reset_token.token}/'
+ response = self.client.post(reset_step_2_path, data, follow=True)
+ self.assertIn((self.core_login_with_next, 302), response.redirect_chain)
+
+
+class RegisterTests(NextURLTests):
+
+ @patch('core.views.logic.password_policy_check')
+ @override_settings(CAPTCHA_TYPE='')
+ @override_settings(ENABLE_ORCID=True)
+ def test_register_email_form_valid_redirects_with_next(self, password_check):
+ password_check.return_value = None
+ data = {
+ 'email': 'kjhsaqccxf7qfwirhqia@example.org',
+ 'password_1': 'qsX1roLama3ADotEopfq',
+ 'password_2': 'qsX1roLama3ADotEopfq',
+ 'first_name': 'New',
+ 'last_name': 'User',
+ 'next': self.next_url_raw,
+ }
+ response = self.client.post('/register/step/1/', data, follow=True)
+ self.assertIn((self.core_login_with_next, 302), response.redirect_chain)
+
+ @patch('core.views.orcid.get_orcid_record_details')
+ @patch('core.views.logic.password_policy_check')
+ @override_settings(CAPTCHA_TYPE='')
+ @override_settings(ENABLE_ORCID=True)
+ def test_user_register_orcid_form_valid_redirects_to_next(
+ self,
+ password_check,
+ get_orcid_details
+ ):
+ get_orcid_details.return_value = {
+ 'first_name': 'New',
+ 'last_name': 'User',
+ 'emails': ['kjhsaqccxf7qfwirhqia@example.org'],
+ }
+ password_check.return_value = None
+ data = {
+ 'first_name': 'New',
+ 'last_name': 'User',
+ 'email': 'kjhsaqccxf7qfwirhqia@example.org',
+ 'password_1': 'qsX1roLama3ADotEopfq',
+ 'password_2': 'qsX1roLama3ADotEopfq',
+ 'token': self.orcid_token_str,
+ 'next': self.next_url_raw,
+ }
+ response = self.client.post('/register/step/1/', data, follow=True)
+ self.assertIn((self.next_url_raw, 302), response.redirect_chain)
+
+
+class OrcidRegistrationTests(NextURLTests):
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ def test_login_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ orcid_registration_path = f'/register/step/orcid/{self.orcid_token_str}/'
+ response = self.client.get(orcid_registration_path, data)
+ self.assertIn(
+ f'/login/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ def test_forgot_password_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ orcid_registration_path = f'/register/step/orcid/{self.orcid_token_str}/'
+ response = self.client.get(orcid_registration_path, data)
+ self.assertIn(
+ f'/reset/step/1/?next={self.next_url_encoded}',
+ response.content.decode(),
+ )
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ def test_register_link_has_next(self, get_theme_dirs):
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ orcid_registration_path = f'/register/step/orcid/{self.orcid_token_str}/'
+ response = self.client.get(orcid_registration_path, data)
+ self.assertIn(
+ f'/register/step/1/?next={self.next_url_encoded}&token={self.orcid_token_str}',
+ response.content.decode(),
+ )
+
+
+class ActivateAccountTests(NextURLTests):
+
+ @patch('core.views.models.Account.objects.get')
+ def test_activate_success_redirects_to_next(self, objects_get):
+ objects_get.return_value = self.user
+ data = {
+ 'next': self.next_url_raw,
+ }
+ response = self.client.post('/register/step/2/12345/', data, follow=True)
+ self.assertIn((self.core_login_with_next, 302), response.redirect_chain)
+
+ @patch('utils.template_override_middleware.Loader.get_theme_dirs')
+ @patch('core.views.models.Account.objects.get')
+ def test_login_link_has_next(self, objects_get, get_theme_dirs):
+ objects_get.return_value = None
+ data = {
+ 'next': self.next_url_raw,
+ }
+ for theme_dirs in self.theme_dirs:
+ get_theme_dirs.return_value = theme_dirs
+ response = self.client.get('/register/step/2/12345/', data)
+ self.assertIn(
+ self.core_login_with_next,
+ response.content.decode(),
+ )
diff --git a/src/core/views.py b/src/core/views.py
index c5ad018bcf..f42b7aae6e 100755
--- a/src/core/views.py
+++ b/src/core/views.py
@@ -6,6 +6,7 @@
from importlib import import_module
import json
+from urllib.parse import unquote, urlencode
import pytz
import time
@@ -64,10 +65,11 @@ def user_login(request):
:param request: HttpRequest
:return: HttpResponse
"""
+ next_url = request.GET.get('next', '') or request.POST.get('next', '')
if request.user.is_authenticated:
messages.info(request, 'You are already logged in.')
- if request.GET.get('next'):
- return redirect(request.GET.get('next'))
+ if next_url:
+ return redirect(next_url)
else:
return redirect(reverse('website_index'))
else:
@@ -87,10 +89,10 @@ def user_login(request):
form = forms.LoginForm(request.POST, bad_logins=bad_logins)
if form.is_valid():
- user = request.POST.get('user_name').lower()
- pawd = request.POST.get('user_pass')
+ username = request.POST.get('user_name').lower()
+ password = request.POST.get('user_pass')
- user = authenticate(username=user, password=pawd)
+ user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
@@ -107,8 +109,8 @@ def user_login(request):
except models.OrcidToken.DoesNotExist:
pass
- if request.GET.get('next'):
- return redirect(request.GET.get('next'))
+ if next_url:
+ return redirect(next_url)
elif request.journal:
return redirect(reverse('core_dashboard'))
else:
@@ -148,29 +150,61 @@ def user_login(request):
def user_login_orcid(request):
"""
- Allows a user to login with ORCiD
+ Allows a user to login with ORCiD.
+ Redirects them to the Janeway login page if ORCID is not enabled.
+ Sends them to orcid.org for authorization if needed.
+ Logs them in when returned here with the right details.
:param request: HttpRequest object
:return: HttpResponse object
"""
- orcid_code = request.GET.get('code', None)
action = request.GET.get('state', 'login')
+ orcid_code = request.GET.get('code', '') or request.POST.get('code', '')
+ state = request.GET.get('state', '') or request.POST.get('state', '')
+ next_url = state or request.GET.get('next', '') or request.POST.get('next', '')
- if orcid_code and django_settings.ENABLE_ORCID:
+ if not django_settings.ENABLE_ORCID:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('ORCID is not enabled.'
+ 'Please log in with your username and password.')
+ )
+ return redirect(
+ logic.reverse_with_next('core_login', request, next_url=next_url)
+ )
+
+ elif not orcid_code:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ _('No authorisation code provided.'
+ 'Please try again or log in with your username and password.')
+ )
+ base = django_settings.ORCID_URL
+ query_dict = {
+ 'client_id': django_settings.ORCID_CLIENT_ID,
+ 'response_type': 'code',
+ 'scope': '/authenticate',
+ 'redirect_uri': orcid.build_redirect_uri(request.site_type),
+ 'state': next_url,
+ }
+ orcid_login_url = f'{base}?{urlencode(query_dict)}'
+ return redirect(orcid_login_url)
+
+ else:
orcid_id = orcid.retrieve_tokens(
orcid_code,
request.site_type,
action=action
)
-
if orcid_id:
try:
user = models.Account.objects.get(orcid=orcid_id)
if action == 'login':
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
-
- if request.GET.get('next'):
- return redirect(request.GET.get('next'))
+ if next_url:
+ return redirect(next_url)
elif request.journal:
return redirect(reverse('core_dashboard'))
else:
@@ -185,8 +219,8 @@ def user_login_orcid(request):
# Store ORCID for future authentication requests
candidates.update(orcid=orcid_id)
login(request, candidates.first())
- if request.GET.get('next'):
- return redirect(request.GET.get('next'))
+ if next_url:
+ return redirect(next_url)
elif request.journal:
return redirect(reverse('core_dashboard'))
else:
@@ -199,20 +233,24 @@ def user_login_orcid(request):
if action == 'register':
return redirect(reverse('core_register') + f'?token={new_token.token}')
else:
- return redirect(reverse('core_orcid_registration', kwargs={'token': new_token.token}))
+ return redirect(
+ logic.reverse_with_next(
+ 'core_orcid_registration',
+ request,
+ next_url=next_url,
+ kwargs={'token': new_token.token}
+ )
+ )
else:
messages.add_message(
request,
messages.WARNING,
- 'Valid ORCiD not returned, please try again, or login with your username and password.'
+ 'Valid ORCiD not returned. '
+ 'Please try again, or log in with your username and password.'
+ )
+ return redirect(
+ logic.reverse_with_next('core_login', request, next_url=next_url)
)
- return redirect(reverse('core_login'))
- else:
- messages.add_message(
- request,
- messages.WARNING,
- 'No authorisation code provided, please try again or login with your username and password.')
- return redirect(reverse('core_login'))
@login_required
@@ -250,9 +288,9 @@ def get_reset_token(request):
try:
account = models.Account.objects.get(email__iexact=email_address)
logic.start_reset_process(request, account)
- return redirect(reverse('core_login'))
+ return redirect(logic.reverse_with_next('core_login', request))
except models.Account.DoesNotExist:
- return redirect(reverse('core_login'))
+ return redirect(logic.reverse_with_next('core_login', request))
template = 'core/accounts/get_reset_token.html'
context = {
@@ -265,7 +303,9 @@ def get_reset_token(request):
def reset_password(request, token):
"""
- Takes a reset token and checks if it is valid then allows a user to reset their password, adter it expires the token
+ Takes a reset token and checks if it is valid.
+ Then it allows a user to reset their password,
+ and finally it expires the token.
:param request: HttpRequest
:param token: string, PasswordResetToken.token
:return: HttpResponse object
@@ -294,7 +334,7 @@ def reset_password(request, token):
reset_token.expired = True
reset_token.save()
messages.add_message(request, messages.SUCCESS, 'Your password has been reset.')
- return redirect(reverse('core_login'))
+ return redirect(logic.reverse_with_next('core_login', request))
template = 'core/accounts/reset_password.html'
context = {
@@ -307,14 +347,19 @@ def reset_password(request, token):
def register(request):
"""
- Displays a form for users to register with the journal. If the user is registering on a journal we give them
+ Displays a form for users to register with the journal.
+ If the user is registering on a journal we give them
the Author role.
:param request: HttpRequest object
:return: HttpResponse object
"""
context = {}
initial = {}
- token, token_obj = request.GET.get('token', None), None
+
+ token = request.GET.get('token') or request.POST.get('token')
+ token_obj = None
+ next_url = request.GET.get('next', '') or request.POST.get('next', '')
+
if token:
token_obj = get_object_or_404(models.OrcidToken, token=token)
orcid_details = orcid.get_orcid_record_details(token_obj.orcid)
@@ -358,8 +403,8 @@ def register(request):
new_user.is_active = True
new_user.save()
login(request, new_user)
- if request.GET.get('next'):
- return redirect(request.GET.get('next'))
+ if next_url:
+ return redirect(next_url)
elif request.journal:
return redirect(reverse('core_dashboard'))
else:
@@ -373,10 +418,10 @@ def register(request):
messages.add_message(
request, messages.SUCCESS,
- _('Your account has been created, please follow the'
+ _('Your account has been created. Please follow the '
'instructions in the email that has been sent to you.'),
)
- return redirect(reverse('core_login'))
+ return redirect(logic.reverse_with_next('core_login', request))
template = 'core/accounts/register.html'
context["form"] = form
@@ -419,7 +464,7 @@ def activate_account(request, token):
_('Account activated'),
)
- return redirect(reverse('core_login'))
+ return redirect(logic.reverse_with_next('core_login', request))
template = 'core/accounts/activate_account.html'
context = {
@@ -459,7 +504,8 @@ def edit_profile(request):
try:
validate_email(email_address)
try:
- logic.handle_email_change(request, email_address)
+ next_url = reverse('core_edit_profile')
+ logic.handle_email_change(request, email_address, next_url=next_url)
return redirect(reverse('website_index'))
except IntegrityError:
messages.add_message(
diff --git a/src/journal/models.py b/src/journal/models.py
index 1e7c44e33e..25661861a1 100644
--- a/src/journal/models.py
+++ b/src/journal/models.py
@@ -7,6 +7,7 @@
from itertools import chain
from operator import itemgetter
import collections
+from urllib.parse import parse_qs
import uuid
import os
import re
diff --git a/src/press/forms.py b/src/press/forms.py
index cfcbe5e5db..f035ce5f9c 100755
--- a/src/press/forms.py
+++ b/src/press/forms.py
@@ -39,8 +39,6 @@ class Meta:
'password_number',
'password_upper',
'password_length',
- 'password_reset_text',
- 'registration_text',
'tracking_code',
'disable_journals',
'privacy_policy_url',
@@ -51,8 +49,6 @@ class Meta:
),
'footer_description': TinyMCE(),
'journal_footer_text': TinyMCE(),
- 'password_reset_text': TinyMCE(),
- 'registration_text': TinyMCE(),
'description': TinyMCE(),
}
diff --git a/src/press/models.py b/src/press/models.py
index 20c19ff192..60ff035090 100755
--- a/src/press/models.py
+++ b/src/press/models.py
@@ -129,8 +129,6 @@ class Press(AbstractSiteModel):
help_text="URL to an external privacy-policy, linked from the page"
" footer. If blank, it links to the Janeway CMS page: /site/privacy.",
)
- password_reset_text = JanewayBleachField(blank=True, null=True, default=press_text('reset'))
- registration_text = JanewayBleachField(blank=True, null=True, default=press_text('registration'))
password_number = models.BooleanField(default=False, help_text='If set, passwords must include one number.')
password_upper = models.BooleanField(default=False, help_text='If set, passwords must include one upper case.')
diff --git a/src/security/decorators.py b/src/security/decorators.py
index 22bebb9f44..a73994a609 100755
--- a/src/security/decorators.py
+++ b/src/security/decorators.py
@@ -49,7 +49,10 @@ def base_check(request, login_redirect=False):
):
if login_redirect is True:
request_params = request.GET.urlencode()
- params = urlencode({"next": f"{request.path}?{request_params}"})
+ if request_params:
+ params = urlencode({"next": f"{request.path}?{request_params}"})
+ else:
+ params = urlencode({"next": request.path})
return redirect('{0}?{1}'.format(reverse('core_login'), params))
elif isinstance(login_redirect, str):
params = urlencode({"next": login_redirect})
diff --git a/src/templates/admin/journal/submissions.html b/src/templates/admin/journal/submissions.html
index dd616c2c48..26f33c220e 100644
--- a/src/templates/admin/journal/submissions.html
+++ b/src/templates/admin/journal/submissions.html
@@ -32,8 +32,12 @@
{% if not journal_settings.general.disable_journal_submission %}
-{% endblock body %}
\ No newline at end of file
+{% endblock body %}
diff --git a/src/themes/OLH/templates/core/accounts/orcid_registration.html b/src/themes/OLH/templates/core/accounts/orcid_registration.html
index 118ab6e41d..29ce8f935a 100644
--- a/src/themes/OLH/templates/core/accounts/orcid_registration.html
+++ b/src/themes/OLH/templates/core/accounts/orcid_registration.html
@@ -1,6 +1,7 @@
{% extends "core/base.html" %}
{% load foundation %}
{% load i18n %}
+{% load next_url %}
{% block title %}{% trans "Unregistered ORCiD" %}{% endblock title %}
@@ -16,16 +17,30 @@
{% block body %}
-
{% trans "Unregistered ORCiD" %}
-
{% trans "The ORCiD you logged in with is not currently linked with an account in our system. You can either register a new account, or login with an existing account to link your ORCiD for future use." %}
-
-{% endblock body %}
\ No newline at end of file
+{% endblock body %}
diff --git a/src/themes/OLH/templates/core/base.html b/src/themes/OLH/templates/core/base.html
index c82da378a9..f93dbca599 100644
--- a/src/themes/OLH/templates/core/base.html
+++ b/src/themes/OLH/templates/core/base.html
@@ -5,6 +5,7 @@
{% load i18n %}
{% load roles %}
{% load hooks %}
+{% load next_url %}
@@ -139,7 +140,16 @@
{% endif %}
{% else %}
-
{% trans "Login" %} | {% trans "Register" %}
+
+
+
+ {% trans "Log in" %}
+ |
+
+ {% trans "Register" %}
+
+
+
{% endif %}
diff --git a/src/themes/OLH/templates/core/login.html b/src/themes/OLH/templates/core/login.html
index 669cef1b3b..a44503c62a 100644
--- a/src/themes/OLH/templates/core/login.html
+++ b/src/themes/OLH/templates/core/login.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load orcid %}
{% load fqdn %}
+{% load next_url %}
{% block title %}{% trans "Login" %}{% endblock title %}
@@ -25,10 +26,18 @@
diff --git a/src/themes/OLH/templates/core/nav.html b/src/themes/OLH/templates/core/nav.html
index 658a4605bf..3b33121e33 100644
--- a/src/themes/OLH/templates/core/nav.html
+++ b/src/themes/OLH/templates/core/nav.html
@@ -1,6 +1,7 @@
{% load roles %}
{% load i18n %}
{% load hooks %}
+{% load next_url %}
diff --git a/src/themes/OLH/templates/elements/journal_footer.html b/src/themes/OLH/templates/elements/journal_footer.html
index fb3ff3b65e..40d8469a74 100644
--- a/src/themes/OLH/templates/elements/journal_footer.html
+++ b/src/themes/OLH/templates/elements/journal_footer.html
@@ -27,7 +27,13 @@
{% trans "Privacy Policy" %}
{% endif %}
{% trans "Contact" %}
- {% if not request.user.is_authenticated %}
{% trans 'Login' %} {% endif %}
+ {% if not request.user.is_authenticated %}
+
+
+ {% trans 'Log in' %}
+
+
+ {% endif %}
{% if journal_settings.general.switch_language %}
{% else %}
-
{% trans 'Login' %}
-
{% trans 'Register' %}
+
+
+ {% trans 'Log in' %}
+
+
+
+
+ {% trans 'Register' %}
+
+
{% endif %}
diff --git a/src/themes/clean/templates/elements/journal_footer.html b/src/themes/clean/templates/elements/journal_footer.html
index eb6d815eb0..e6053919a8 100644
--- a/src/themes/clean/templates/elements/journal_footer.html
+++ b/src/themes/clean/templates/elements/journal_footer.html
@@ -39,8 +39,9 @@
{% trans "Contact" %}
{% if not request.user.is_authenticated %}
-
- {% trans 'Login' %}
+
+ {% trans 'Log in' %}
+
{% endif %}
{% if journal_settings.general.switch_language %}
diff --git a/src/themes/clean/templates/elements/press_footer.html b/src/themes/clean/templates/elements/press_footer.html
index 8f5a35e662..216925a94f 100644
--- a/src/themes/clean/templates/elements/press_footer.html
+++ b/src/themes/clean/templates/elements/press_footer.html
@@ -16,7 +16,12 @@
{% trans "Sitemap" %}
{% trans "Contact" %}
{% if not request.user.is_authenticated %}
-
{% trans 'Login' %}> {% endif %}
+
+
+ {% trans 'Log in' %}
+
+
+ {% endif %}
{% if journal_settings.general.switch_language %}
{% csrf_token %}
diff --git a/src/themes/clean/templates/journal/become_reviewer.html b/src/themes/clean/templates/journal/become_reviewer.html
index 66257a5554..1d10b1dc90 100644
--- a/src/themes/clean/templates/journal/become_reviewer.html
+++ b/src/themes/clean/templates/journal/become_reviewer.html
@@ -1,5 +1,6 @@
{% extends "core/base.html" %}
{% load i18n %}
+{% load next_url %}
{% block page_title %}{% trans "Become a Reviewer" %}{% endblock %}
{% block title %}{% trans "Become a Reviewer" %}{% endblock %}
@@ -9,8 +10,9 @@
{% if not code == 'already-reviewer' %}
{% if code == 'not-logged-in' %}
-
- {% trans "Login" %}
+
+ {% trans "Log in" %}
+
{% else %}
{% csrf_token %}
@@ -18,4 +20,4 @@
{% endif %}
{% endif %}
-{% endblock body %}
\ No newline at end of file
+{% endblock body %}
diff --git a/src/themes/clean/templates/journal/submissions.html b/src/themes/clean/templates/journal/submissions.html
index bceee068a2..61dd0962e9 100644
--- a/src/themes/clean/templates/journal/submissions.html
+++ b/src/themes/clean/templates/journal/submissions.html
@@ -15,7 +15,9 @@ {% trans 'Submissions' %}
diff --git a/src/themes/clean/templates/press/nav.html b/src/themes/clean/templates/press/nav.html
index 36d8a9ef04..352ef3f4a0 100644
--- a/src/themes/clean/templates/press/nav.html
+++ b/src/themes/clean/templates/press/nav.html
@@ -73,10 +73,18 @@
{% else %}
-
- {% trans 'Login' %}
-
- {% trans 'Register' %}
+
+
+
+ {% trans 'Log in' %}
+
+
+
+
+
+ {% trans 'Register' %}
+
+
{% endif %}
diff --git a/src/themes/material/templates/core/accounts/activate_account.html b/src/themes/material/templates/core/accounts/activate_account.html
index 6547562aef..63b1487e87 100644
--- a/src/themes/material/templates/core/accounts/activate_account.html
+++ b/src/themes/material/templates/core/accounts/activate_account.html
@@ -1,6 +1,7 @@
{% extends "core/base.html" %}
{% load foundation %}
{% load i18n %}
+{% load next_url %}
{% block title %}{% trans 'Activate Account' %}{% endblock title %}
@@ -22,9 +23,15 @@
{% else %}
{% trans "Error" %}
- {% trans "There was no inactive account with this activation code found. It is possible that your account is already active, you can check by attempting to " %}
- {% trans "login" %} .
+ {% blocktrans %}
+ There was no inactive account with this activation code found. It
+ is possible that your account is already active. You can check by
+ attempting to log in.
+ {% endblocktrans %}
+
+ {% trans "Log in" %}
+
{% endif %}
diff --git a/src/themes/material/templates/core/accounts/orcid_registration.html b/src/themes/material/templates/core/accounts/orcid_registration.html
new file mode 100644
index 0000000000..932b6a3d88
--- /dev/null
+++ b/src/themes/material/templates/core/accounts/orcid_registration.html
@@ -0,0 +1,59 @@
+{% extends "core/base.html" %}
+{% load materializecss %}
+{% load i18n %}
+{% load static %}
+{% load next_url %}
+
+{% block title %}{% trans "Unregistered ORCiD" %}{% endblock title %}
+
+{% block body %}
+
+
+
+
+
+
+ {% trans "Unregistered ORCiD" %}
+
+
{% blocktrans %}
+ The ORCiD you logged in with is not
+ currently linked with an account in our system. You can
+ either register a new account, or login with an existing
+ account to link your ORCiD for future use.
+ {% endblocktrans %}
+
+
+ {% trans "Register" %}
+
+
+
+ {% include "elements/forms/errors.html" %}
+ {% csrf_token %}
+
+ {% trans 'Email' %}
+
+
+
+ {% trans 'Password' %}
+
+
+
+
+
+ {% trans 'Log In' %}
+
+
+
+
+ {% trans "Reset Your Password" %}
+
+
+
+
+
+
+
+
+{% endblock body %}
diff --git a/src/themes/material/templates/core/login.html b/src/themes/material/templates/core/login.html
index 3020fa0523..2d7d0656f3 100644
--- a/src/themes/material/templates/core/login.html
+++ b/src/themes/material/templates/core/login.html
@@ -1,6 +1,7 @@
{% extends "core/base.html" %}
{% load i18n %}
{% load orcid %}
+{% load next_url %}
{% block title %}{% trans "Login" %}{% endblock title %}
@@ -13,14 +14,20 @@
{% csrf_token %}
{% trans "Login with your account" %}
- {% if settings.ENABLE_ORCID %}
-
{% trans "Log in with ORCiD" %}
+ {% if settings.ENABLE_ORCID %}
+
+ {% trans "Log in with ORCiD" %}
+
{% endif %}
{% if settings.ENABLE_OIDC %}
-
{% trans "Login with" %} {{ settings.OIDC_SERVICE_NAME }}
-
+
+ {% trans "Login with" %} {{ settings.OIDC_SERVICE_NAME }}
+
+
{% endif %}
{% if request.repository and request.repository.login_text %}
{{ request.repository.login_text|safe }}
@@ -42,12 +49,15 @@
{% trans "Login" %}
- {% trans "Register an Account" %}
+
+ {% trans "Register an Account" %}
+
- {% trans "Reset Your Password" %}
+
+ {% trans "Reset Your Password" %}
+
-
diff --git a/src/themes/material/templates/core/nav.html b/src/themes/material/templates/core/nav.html
index 5d949cb5a2..9aa53a7760 100644
--- a/src/themes/material/templates/core/nav.html
+++ b/src/themes/material/templates/core/nav.html
@@ -4,6 +4,7 @@
{% load roles %}
{% load i18n %}
{% load hooks %}
+{% load next_url %}