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', ] }, 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 8a87880e6c..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 "" @@ -2435,13 +2435,15 @@ msgstr "" msgid "" "All authors must agree to the below statements in order to submit an article " "to" -msgstr "" +msgstr "Alle auteurs moeten zich akkoord verklaren met onderstaande stellingen " +"om een artikel te publiceren in" #: src/templates/admin/submission/start.html:24 msgid "" "If you do not agree with these terms you will be unable to proceed with your " "submission." -msgstr "" +msgstr "Als je je niet akkoord verklaart met deze voorwaarden " +"kan je niet verdergaan met deze inzending." #: src/templates/admin/submission/start.html:29 msgid "Publication Fees" 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/migrations/0096_alter_account_activation_code_and_more.py b/src/core/migrations/0096_alter_account_activation_code_and_more.py index 424abfd9c7..e9221a26f0 100644 --- a/src/core/migrations/0096_alter_account_activation_code_and_more.py +++ b/src/core/migrations/0096_alter_account_activation_code_and_more.py @@ -3,6 +3,8 @@ import core.model_utils from django.db import migrations, models +import utils + class Migration(migrations.Migration): @@ -19,32 +21,32 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='account', name='department', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300, verbose_name='Department'), + field=models.CharField(blank=True, max_length=300, verbose_name='Department', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', name='first_name', - field=core.model_utils.JanewayBleachCharField(max_length=300, verbose_name='First name'), + field=models.CharField(max_length=300, verbose_name='First name', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', name='institution', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=1000, verbose_name='Institution'), + field=models.CharField(blank=True, max_length=1000, verbose_name='Institution', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', name='last_name', - field=core.model_utils.JanewayBleachCharField(max_length=300, verbose_name='Last name'), + field=models.CharField(max_length=300, verbose_name='Last name', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', name='middle_name', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300, verbose_name='Middle name'), + field=models.CharField(blank=True, max_length=300, verbose_name='Middle name', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', name='salutation', - field=core.model_utils.JanewayBleachCharField(blank=True, choices=[('Miss', 'Miss'), ('Ms', 'Ms'), ('Mrs', 'Mrs'), ('Mr', 'Mr'), ('Mx', 'Mx'), ('Dr', 'Dr'), ('Prof.', 'Prof.')], max_length=10, verbose_name='Salutation'), + field=models.CharField(blank=True, choices=[('Miss', 'Miss'), ('Ms', 'Ms'), ('Mrs', 'Mrs'), ('Mr', 'Mr'), ('Mx', 'Mx'), ('Dr', 'Dr'), ('Prof.', 'Prof.')], max_length=10, verbose_name='Salutation', validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='account', @@ -54,6 +56,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='account', name='suffix', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300, verbose_name='Name suffix'), + field=models.CharField(blank=True, max_length=300, verbose_name='Name suffix', validators=[utils.forms.plain_text_validator]), ), ] 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, + ) + ] diff --git a/src/core/models.py b/src/core/models.py index 9a3661a4ba..9cec7725f4 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -53,6 +53,7 @@ from submission import models as submission_models from utils.logger import get_logger from utils import logic as utils_logic +from utils.forms import plain_text_validator from production import logic as production_logic fs = JanewayFileSystemStorage() @@ -235,48 +236,56 @@ class Account(AbstractBaseUser, PermissionsMixin): username = models.CharField(max_length=254, unique=True, verbose_name=_('Username')) name_prefix = models.CharField(max_length=10, blank=True) - first_name = JanewayBleachCharField( + first_name = models.CharField( max_length=300, blank=False, verbose_name=_('First name'), + validators=[plain_text_validator], ) - middle_name = JanewayBleachCharField( + middle_name = models.CharField( max_length=300, blank=True, verbose_name=_('Middle name'), + validators=[plain_text_validator], ) - last_name = JanewayBleachCharField( + last_name = models.CharField( max_length=300, blank=False, verbose_name=_('Last name'), + validators=[plain_text_validator], ) + # activation_code is deprecated activation_code = models.CharField(max_length=100, null=True, blank=True) - salutation = JanewayBleachCharField( + salutation = models.CharField( max_length=10, choices=SALUTATION_CHOICES, blank=True, verbose_name=_('Salutation'), + validators=[plain_text_validator], ) - suffix = JanewayBleachCharField( + suffix = models.CharField( max_length=300, blank=True, verbose_name=_('Name suffix'), + validators=[plain_text_validator], ) biography = JanewayBleachField( blank=True, verbose_name=_('Biography'), ) orcid = models.CharField(max_length=40, null=True, blank=True, verbose_name=_('ORCiD')) - institution = JanewayBleachCharField( + institution = models.CharField( max_length=1000, blank=True, verbose_name=_('Institution'), + validators=[plain_text_validator], ) - department = JanewayBleachCharField( + department = models.CharField( max_length=300, blank=True, verbose_name=_('Department'), + validators=[plain_text_validator], ) twitter = models.CharField(max_length=300, null=True, blank=True, verbose_name=_('Twitter Handle')) facebook = models.CharField(max_length=300, null=True, blank=True, verbose_name=_('Facebook Handle')) @@ -286,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/templatetags/orcid.py b/src/core/templatetags/orcid.py index 69f5bb016f..a44a11ea3f 100644 --- a/src/core/templatetags/orcid.py +++ b/src/core/templatetags/orcid.py @@ -6,7 +6,7 @@ @register.simple_tag(takes_context=True) -def orcid_redirect_uri(context, action): +def orcid_redirect_uri(context, action="login"): request = context.get('request') if request: return build_redirect_uri(request.site_type, action=action) diff --git a/src/core/templatetags/roles.py b/src/core/templatetags/roles.py index 3b43d5e1a0..f972c7e388 100755 --- a/src/core/templatetags/roles.py +++ b/src/core/templatetags/roles.py @@ -2,6 +2,9 @@ from core import models +from submission import models + + register = template.Library() 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/journal/views.py b/src/journal/views.py index f595254f72..69e518d153 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 @@ -838,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. @@ -2432,7 +2443,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, 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 5112e58de5..a73994a609 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 @@ -43,10 +49,13 @@ 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": redirect}) + params = urlencode({"next": login_redirect}) return redirect('{0}?{1}'.format(reverse('core_login'), params)) else: return False @@ -269,7 +278,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): @@ -288,6 +296,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 identifiable 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. diff --git a/src/security/logic.py b/src/security/logic.py index 2afb4b21d7..277b7c4299 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,29 @@ 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 + + # early return if filter not enabled + if not se_pii_filter_enabled: + 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 + # to view the actual value. + stages = [ + submission_models.STAGE_UNASSIGNED, + submission_models.STAGE_ASSIGNED, + submission_models.STAGE_UNDER_REVIEW, + submission_models.STAGE_UNDER_REVISION, + ] + if request.user in article.section_editors() and article.stage in stages: + return False + return True diff --git a/src/security/templatetags/securitytags.py b/src/security/templatetags/securitytags.py index 5f5d6fcb7a..52599950c2 100755 --- a/src/security/templatetags/securitytags.py +++ b/src/security/templatetags/securitytags.py @@ -1,5 +1,8 @@ from django import template + +from core.middleware import GlobalRequestMiddleware from security import logic +from django.utils.translation import gettext_lazy as _ register = template.Library() @@ -87,3 +90,26 @@ def is_repository_manager(context): def is_preprint_editor(context): request = context['request'] return request.user.is_preprint_editor(request) + + +@register.filter +def se_can_see_pii(value, article): + request = GlobalRequestMiddleware.get_current_request() + + if logic.can_see_pii(request, article): + return value + else: + return _('[Anonymised data]') + + +@register.simple_tag(takes_context=True) +def can_see_pii_tag(context, article): + request = context.get('request') + + if logic.can_see_pii(request, article): + return True + else: + return False + + + diff --git a/src/security/test_security.py b/src/security/test_security.py index 2510b9a905..c1ab2e72b2 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,23 @@ 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): + 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, + msg="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) @@ -3967,19 +3984,105 @@ 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', + '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, + ) + + 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)) + + 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=kwargs, + ), + 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)) + # General helper functions @staticmethod - def create_user(username, roles=None, journal=None): + def create_user( + username, + roles=None, + 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, + 'institution': institution, + 'department': department, + 'orcid': orcid, + 'country': country_obj, + } ) @staticmethod @@ -4007,6 +4110,28 @@ 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(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) + pii_strings.append(article.correspondence_author.department) + pii_strings.append( + 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 def setUpTestData(self): """ @@ -4018,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() @@ -4040,7 +4174,18 @@ 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( + "b.torres@voyager.com", + ["author"], + 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() @@ -4107,7 +4252,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() @@ -4150,6 +4299,27 @@ 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, + 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, diff --git a/src/submission/forms.py b/src/submission/forms.py index 6778cc00c3..604012de7f 100755 --- a/src/submission/forms.py +++ b/src/submission/forms.py @@ -33,8 +33,12 @@ class ArticleStart(forms.ModelForm): class Meta: model = models.Article - fields = ('publication_fees', 'submission_requirements', 'copyright_notice', - 'competing_interests') + fields = ( + 'publication_fees', + 'submission_requirements', + 'copyright_notice', + 'competing_interests', + ) def __init__(self, *args, **kwargs): journal = kwargs.pop('journal', False) @@ -77,7 +81,7 @@ class Meta: 'language', 'section', 'license', 'primary_issue', 'article_number', 'is_remote', 'remote_url', 'peer_reviewed', 'first_page', 'last_page', 'page_numbers', 'total_pages', - 'competing_interests', 'custom_how_to_cite', 'rights', + 'custom_how_to_cite', 'rights', ) widgets = { 'title': forms.TextInput(attrs={'placeholder': _('Title')}), @@ -208,7 +212,6 @@ def __init__(self, *args, **kwargs): if editor_view: self.fields[element.name].required = False - def save(self, commit=True, request=None): article = super(ArticleInfo, self).save(commit=False) @@ -252,6 +255,11 @@ def __init__(self, *args, **kwargs): "closed for public submission" +class EditArticleMetadata(ArticleInfo): + class Meta(ArticleInfo.Meta): + fields = ArticleInfo.Meta.fields + ('competing_interests',) + + class AuthorForm(forms.ModelForm): class Meta: diff --git a/src/submission/migrations/0080_frozen_author_bleach_20240507_1350.py b/src/submission/migrations/0080_frozen_author_bleach_20240507_1350.py index 2cc7ae6838..d49a0fb616 100644 --- a/src/submission/migrations/0080_frozen_author_bleach_20240507_1350.py +++ b/src/submission/migrations/0080_frozen_author_bleach_20240507_1350.py @@ -2,6 +2,7 @@ import core.model_utils from django.db import migrations, models +import utils.forms class Migration(migrations.Migration): @@ -15,12 +16,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='frozenauthor', name='department', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300), + field=models.CharField(blank=True, max_length=300, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', name='first_name', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300), + field=models.CharField(blank=True, max_length=300, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', @@ -40,26 +41,26 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='frozenauthor', name='institution', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=1000), + field=models.CharField(blank=True, max_length=1000, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', name='last_name', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300), + field=models.CharField(blank=True, max_length=300, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', name='middle_name', - field=core.model_utils.JanewayBleachCharField(blank=True, max_length=300), + field=models.CharField(blank=True, max_length=300, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', name='name_prefix', - field=core.model_utils.JanewayBleachCharField(blank=True, help_text='Optional name prefix (e.g: Prof or Dr)', max_length=300), + field=models.CharField(blank=True, help_text='Optional name prefix (e.g: Prof or Dr)', max_length=300, validators=[utils.forms.plain_text_validator]), ), migrations.AlterField( model_name='frozenauthor', name='name_suffix', - field=core.model_utils.JanewayBleachCharField(blank=True, help_text='Optional name suffix (e.g.: Jr or III)', max_length=300), + field=models.CharField(blank=True, help_text='Optional name suffix (e.g.: Jr or III)', max_length=300, validators=[utils.forms.plain_text_validator]), ), ] diff --git a/src/submission/models.py b/src/submission/models.py index a9b6d80c9b..80e9537765 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -54,6 +54,7 @@ from review import models as review_models from utils.function_cache import cache from utils.logger import get_logger +from utils.forms import plain_text_validator from journal import models as journal_models from review.const import ( ReviewerDecisions as RD, @@ -1911,23 +1912,44 @@ class FrozenAuthor(AbstractLastModifiedModel): on_delete=models.SET_NULL, ) - name_prefix = JanewayBleachCharField( + name_prefix = models.CharField( max_length=300, blank=True, - help_text=_("Optional name prefix (e.g: Prof or Dr)") - - ) - name_suffix = JanewayBleachCharField( + help_text=_("Optional name prefix (e.g: Prof or Dr)"), + validators=[plain_text_validator], + ) + name_suffix = models.CharField( + max_length=300, + blank=True, + help_text=_("Optional name suffix (e.g.: Jr or III)"), + validators=[plain_text_validator], + ) + first_name = models.CharField( max_length=300, blank=True, - help_text=_("Optional name suffix (e.g.: Jr or III)") + validators=[plain_text_validator], + ) + middle_name = models.CharField( + max_length=300, + blank=True, + validators=[plain_text_validator], ) - first_name = JanewayBleachCharField(max_length=300, blank=True) - middle_name = JanewayBleachCharField(max_length=300, blank=True) - last_name = JanewayBleachCharField(max_length=300, blank=True) + last_name = models.CharField( + max_length=300, + blank=True, + validators=[plain_text_validator], +) - institution = JanewayBleachCharField(max_length=1000, blank=True) - department = JanewayBleachCharField(max_length=300, blank=True) + institution = models.CharField( + max_length=1000, + blank=True, + validators=[plain_text_validator], +) + department = models.CharField( + max_length=300, + blank=True, + validators=[plain_text_validator], + ) frozen_biography = JanewayBleachField( blank=True, verbose_name=_('Frozen Biography'), diff --git a/src/submission/tests.py b/src/submission/tests.py index 6b4f57a2ea..de565fe4ba 100644 --- a/src/submission/tests.py +++ b/src/submission/tests.py @@ -13,6 +13,7 @@ from django.utils import translation, timezone from django.urls.base import clear_script_prefix from django.conf import settings +from django.core.exceptions import ValidationError from django.shortcuts import reverse from django.test.utils import override_settings @@ -567,6 +568,33 @@ def test_author_form_with_good_orcid(self): '0000-0003-2126-266X', ) + def test_author_form_harmful_inputs(self): + harmful_string = ' This are not the droids you are looking for ' + for i, attr in enumerate({ + "first_name", + "last_name", + "middle_name", + "name_prefix", + "suffix", + "institution", + "department", + }): + form = forms.AuthorForm( + { + 'first_name': 'Andy', + 'last_name': 'Byers', + 'biography': 'Andy', + 'institution': 'Birkbeck, University of London', + 'email': f'andy{i}@janeway.systems', + 'orcid': 'https://orcid.org/0000-0003-2126-266X', + **{attr: harmful_string}, + } + ) + self.assertFalse( + form.is_valid(), + f"Harmful code injected into field '{attr}'" + ) + @override_settings(URL_CONFIG='domain') def test_article_encoding_bibtex(self): article = helpers.create_article( @@ -906,3 +934,30 @@ def setUpTestData(cls): def test_full_name(self): self.assertEqual('Dr. S. Bella Rogers Esq.', self.frozen_author.full_name()) + + +class ArticleFormTests(TestCase): + + def test_competing_interests_in_edit_article_metadata(self): + form = forms.EditArticleMetadata() + self.assertIn( + 'competing_interests', + form.fields, + "'competing_interests' should be present in EditArticleMetadata", + ) + + def test_competing_interests_not_in_article_info_submit(self): + form = forms.ArticleInfoSubmit() + self.assertNotIn( + 'competing_interests', + form.fields, + "'competing_interests' should NOT be present in ArticleInfoSubmit", + ) + + def test_competing_interests_not_in_editor_article_info_submit(self): + form = forms.EditorArticleInfoSubmit() + self.assertNotIn( + 'competing_interests', + form.fields, + "'competing_interests' should NOT be present in EditorArticleInfoSubmit" + ) diff --git a/src/submission/views.py b/src/submission/views.py index cd2fa76161..dc0afb4f74 100755 --- a/src/submission/views.py +++ b/src/submission/views.py @@ -75,7 +75,13 @@ def start(request, type=None): **{'request': request, 'article': new_article} ) - return redirect(reverse('submit_info', kwargs={'article_id': new_article.pk})) + return redirect( + reverse( + 'submit_info', + kwargs={ + 'article_id': new_article.pk}, + ), + ) template = 'admin/submission/start.html' context = { @@ -697,7 +703,7 @@ def submit_review(request, article_id): return redirect(reverse('core_dashboard')) - template = "admin/submission//submit_review.html" + template = "admin/submission/submit_review.html" context = { 'article': article, 'form': form, @@ -733,7 +739,7 @@ def edit_metadata(request, article_id): article=article, ) - info_form = forms.ArticleInfo( + info_form = forms.EditArticleMetadata( instance=article, additional_fields=additional_fields, submission_summary=submission_summary, @@ -767,7 +773,7 @@ def edit_metadata(request, article_id): return redirect(reverse_url) if 'metadata' in request.POST: - info_form = forms.ArticleInfo( + info_form = forms.EditArticleMetadata( request.POST, instance=article, additional_fields=additional_fields, diff --git a/src/templates/admin/core/dashboard.html b/src/templates/admin/core/dashboard.html index 55ecca7554..d37ad01c73 100644 --- a/src/templates/admin/core/dashboard.html +++ b/src/templates/admin/core/dashboard.html @@ -1,5 +1,5 @@ {% extends "admin/core/base.html" %} -{% load roles %} +{% load roles securitytags %} {% block title %}Dashboard{% endblock title %} {% block title-section %}Dashboard{% endblock %} @@ -192,7 +192,7 @@

Section Editor

{{ assignment.article.safe_title }} - {{ assignment.article.author_list }} + {{ assignment.article.author_list|se_can_see_pii:assignment.article }} {{ assignment.article.date_submitted }} {{ assignment.article.get_stage_display }} diff --git a/src/templates/admin/elements/core/addressee_display.html b/src/templates/admin/elements/core/addressee_display.html index 986cf4fe32..28aec6b086 100644 --- a/src/templates/admin/elements/core/addressee_display.html +++ b/src/templates/admin/elements/core/addressee_display.html @@ -1,8 +1,9 @@ +{% load securitytags %} {% regroup entry.addressees by get_field_display as email_fields %} {% for field, addressees in email_fields %} {% if field %}{{ field }}: {% endif %} {% for addressee in addressees %} - {{ addressee.email }}{% if not forloop.last %}, {% endif %} + {{ addressee.email|se_can_see_pii:article }}{% if not forloop.last %}, {% endif %} {% endfor %} {% if not forloop.last %}
{% endif %} {% endfor %} diff --git a/src/templates/admin/elements/core/submission_list_element.html b/src/templates/admin/elements/core/submission_list_element.html index a61f62293a..3a45164277 100644 --- a/src/templates/admin/elements/core/submission_list_element.html +++ b/src/templates/admin/elements/core/submission_list_element.html @@ -1,12 +1,12 @@ +{% load securitytags %}
  • -

    {{ 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 }}
    - {% for editor in article.editors %}{% if forloop.first %}Editors: {% endif %}{{ editor.editor.full_name }} ( - {% if editor.editor_type == 'section-editor' %}SE{% else %}E - {% endif %}){% if not forloop.last %}, {% endif %}{% endfor %} + Authors: {{ article.author_list|se_can_see_pii:article }}
    + {% for editor in article.editors %}{% if forloop.first %}Editors: {% endif %}{{ editor.editor.full_name }} + ({% if editor.editor_type == 'section-editor' %}SE{% else %}E{% endif %}){% if not forloop.last %}, {% endif %}{% endfor %} View Article diff --git a/src/templates/admin/elements/journal/log_description.html b/src/templates/admin/elements/journal/log_description.html index 206250c669..5d46fcb1d3 100644 --- a/src/templates/admin/elements/journal/log_description.html +++ b/src/templates/admin/elements/journal/log_description.html @@ -1,3 +1,4 @@ +{% load securitytags %}

    @@ -37,7 +38,7 @@

    Subject

    Content

    - {{ entry.description|safe|linebreaksbr }} + {{ entry.description|safe|linebreaksbr|se_can_see_pii:article }}
    diff --git a/src/templates/admin/elements/metadata.html b/src/templates/admin/elements/metadata.html index f5fa18dec1..eb39a58f18 100644 --- a/src/templates/admin/elements/metadata.html +++ b/src/templates/admin/elements/metadata.html @@ -1,11 +1,9 @@ -{% load settings %} -{% load roles %} -{% load static %} -{% load bool_fa %} +{% load settings roles static bool_fa securitytags %} {% user_has_role request 'editor' as user_is_editor %} {% user_has_role request 'section-editor' as user_is_section_editor %} {% user_has_role request 'production' as user_is_production %} +{% can_see_pii_tag article as can_see_pii %} {% if user_is_editor or user_is_production or user_is_section_editor %} {{ article.safe_title }} {{ article.section.name }} {% if article.correspondence_author %} - {{ article.correspondence_author.full_name }}   + + {{ article.correspondence_author.full_name|se_can_see_pii:article }} + {% if can_see_pii %} +   +   + + {% endif %} + {% else %}   This article has no correspondence_author {% endif %} @@ -171,12 +176,25 @@

    Frozen Authors

    {% for f_author in article.frozenauthor_set.all %} - {{ f_author.full_name }} + {{ f_author.full_name|se_can_see_pii:article }} - {% if f_author.author.email %}{{ f_author.author.email }} -   -   - {% elif f_author.email %}{{ f_author.email}}{% else %}No email address recorded.{% endif %} + {% if f_author.author.email %}{{ f_author.author.email|se_can_see_pii:article }} + {% if can_see_pii %} +   +   + {% endif %} + {% elif f_author.email %} + {{ f_author.email|se_can_see_pii:article }} + {% else %} + No email address recorded. + {% endif %} + {{ f_author.display_email|bool_fa }} {% if f_author.author == article.correspondence_author %} diff --git a/src/templates/admin/elements/review/review_block.html b/src/templates/admin/elements/review/review_block.html index 3a82e9a550..6aff9e6122 100644 --- a/src/templates/admin/elements/review/review_block.html +++ b/src/templates/admin/elements/review/review_block.html @@ -36,12 +36,10 @@

    #{{ review.pk }} - {{ review.reviewer.full_name }} {% else %}Awaiting acknowledgement{% endif %} {{ review.date_due|date:"Y-m-d" }} - {% if review.decision %} + {% if review.decision and review.date_complete %} {{ review.get_decision_display|capfirst }}
    - {% if review.date_complete %} - {{ review.date_complete|date:"Y-m-d H:i" }} - {% endif %} + {{ review.date_complete|date:"Y-m-d H:i" }} {% else %} -- {% endif %} diff --git a/src/templates/admin/elements/review/reviews_small.html b/src/templates/admin/elements/review/reviews_small.html index d5203a53ad..73c4b13267 100644 --- a/src/templates/admin/elements/review/reviews_small.html +++ b/src/templates/admin/elements/review/reviews_small.html @@ -26,12 +26,10 @@ {{ review.request_decision_status }} {{ review.date_due|date:"Y-m-d" }} - {% if review.decision %} + {% if review.decision and review.date_complete %} {{ review.get_decision_display|capfirst }}
    - {% if review.date_complete %} - {{ review.date_complete|date:"Y-m-d H:i" }} - {% endif %} + {{ review.date_complete|date:"Y-m-d H:i" }} {% else %} -- {% endif %} diff --git a/src/templates/admin/journal/article_log.html b/src/templates/admin/journal/article_log.html index ff16d20be6..79c0d0e425 100644 --- a/src/templates/admin/journal/article_log.html +++ b/src/templates/admin/journal/article_log.html @@ -1,5 +1,5 @@ {% extends "admin/core/base.html" %} -{% load static %} +{% load static securitytags %} {% block title %}Article {{ article.pk }} Log{% endblock %} {% block title-section %}Article {{ article.pk }} Log{% endblock %} @@ -45,7 +45,7 @@

    Log Entries

    {{ 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 %} 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/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 %}
    {% if not request.user.is_authenticated %} - {% trans 'Register' %} - {% trans 'Login' %} + + {% trans 'Register' %} + + + {% trans 'Log in' %} + {% else %} {% trans 'Start Submission' %} {% endif %} diff --git a/src/templates/admin/press/edit_press.html b/src/templates/admin/press/edit_press.html index 0eb95ed04d..5ab5dda3a4 100644 --- a/src/templates/admin/press/edit_press.html +++ b/src/templates/admin/press/edit_press.html @@ -55,8 +55,6 @@

    Security

    {{ form.password_number|foundation }} {{ form.password_upper|foundation }} {{ form.password_length|foundation }} - {{ form.password_reset_text|foundation }} - {{ form.registration_text|foundation }} {{ form.privacy_policy_url|foundation }} {{ form.tracking_code|foundation }} 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/home.html b/src/templates/admin/review/home.html index c34a0b64c1..7706670e04 100644 --- a/src/templates/admin/review/home.html +++ b/src/templates/admin/review/home.html @@ -1,6 +1,6 @@ {% extends "admin/core/base.html" %} -{% load securitytags %} +{% load securitytags roles %} {% block title %}Articles in Review{% endblock %} @@ -47,7 +47,7 @@

    Articles in Review

    {{ article.pk }} {{ article.safe_title }} {{ article.date_submitted }} - {{ article.correspondence_author.full_name }} + {{ article.correspondence_author.full_name|se_can_see_pii:article }} {% for editor in article.editors %}{{ editor.editor.full_name }}{% if not forloop.last %}, {% endif %}{% endfor %} {{ article.section.name }} {% if article.projected_issue %}{{ article.projected_issue.display_title }}{% else %} diff --git a/src/templates/admin/review/in_review.html b/src/templates/admin/review/in_review.html index 2a0a7c0db1..8765beb393 100644 --- a/src/templates/admin/review/in_review.html +++ b/src/templates/admin/review/in_review.html @@ -1,11 +1,9 @@ {% extends "admin/core/base.html" %} -{% load static %} -{% load itertools %} -{% load roles %} +{% load static itertools roles securitytags %} {% block title %}Review {{ article.title }}{% endblock %} {% block title-section %}Peer Review{% 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 c8d9590f42..1d4f06a4ef 100644 --- a/src/templates/admin/review/unassigned_article.html +++ b/src/templates/admin/review/unassigned_article.html @@ -1,11 +1,10 @@ {% extends "admin/core/base.html" %} -{% load static %} -{% load roles %} -{% load i18n %} +{% 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 %} -{% 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 }} @@ -31,9 +30,9 @@

    Summary of Article

    {{ article.section.name }} - {{ article.owner.full_name }} + {{ article.owner.full_name|se_can_see_pii:article }} {{ article.license.short_name }} - {{ article.language }} + {{ article.get_language_display }} Started @@ -104,8 +103,8 @@

    Authors

    {% for order in article.articleauthororder_set.all %} - {{ order.author.full_name }} - {{ order.author.email }} + {{ order.author.full_name|se_can_see_pii:article }} + {{ order.author.email|se_can_see_pii:article }} {% if order.author == article.correspondence_author %} {% else %} {% endif %} @@ -151,7 +150,9 @@

    Files

    Uploaded Download Replace + {% if can_see_pii %} File History + {% endif %} {% if journal_settings.crosscheck.enable %} Similarity Check{% endif %} @@ -168,9 +169,11 @@

    Files

      + {% if can_see_pii %}   + {% endif %} {% if journal_settings.crosscheck.enable %} {% if not article.ithenticate_id %}

    Display Email Link? Institution Correspondence Author - - + Edit + Delete {% for f_author in article.frozen_authors %} - {{ f_author.full_name }} - {{ f_author.email }} + {{ f_author.full_name|se_can_see_pii:article }} + {{ f_author.email|se_can_see_pii:article }} {{ f_author.display_email|bool_fa }} - {{ f_author.institution }} + {{ 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 %} +
    {% csrf_token %}
    + {% if can_see_pii_tag %} {% include "admin/elements/submission/edit_author.html" %} + {% endif %} {% endblock body %} diff --git a/src/templates/admin/submission/start.html b/src/templates/admin/submission/start.html index 41051192b8..af94db3b09 100644 --- a/src/templates/admin/submission/start.html +++ b/src/templates/admin/submission/start.html @@ -62,7 +62,7 @@

    {% trans "Copyright Notice" %}

    {{ journal_settings.general.copyright_notice|safe }}
    {{ form.copyright_notice }}   + for="id_copyright_notice">{{ form.copyright_notice.label|safe }} {% endif %} diff --git a/src/templates/admin/submission/submit_review.html b/src/templates/admin/submission/submit_review.html index 510fd8b325..e4ad6dbe4d 100644 --- a/src/templates/admin/submission/submit_review.html +++ b/src/templates/admin/submission/submit_review.html @@ -85,6 +85,17 @@

    {% trans 'Agreements' %}

    {{ article.license.name }}{% endif %} + {% if request.journal.submissionconfiguration.competing_interests %} + + Competing Interests + + + + {{ article.competing_interests|safe|default:"No competing interests declared" }} + + + {% endif %} + {% for field_answer in article.fieldanswer_set.all %} {{ field_answer.field.name }} diff --git a/src/templates/common/elements/orcid_registration.html b/src/templates/common/elements/orcid_registration.html index ee83a4b710..f16882b260 100644 --- a/src/templates/common/elements/orcid_registration.html +++ b/src/templates/common/elements/orcid_registration.html @@ -9,6 +9,10 @@ [remove]

    {% else %} - {% trans "Register with ORCiD" %} + + {% trans "Register with ORCiD" %} + {% endif %} {% endif %} diff --git a/src/themes/OLH/templates/core/accounts/activate_account.html b/src/themes/OLH/templates/core/accounts/activate_account.html index 68f2bb578f..586f65392c 100644 --- a/src/themes/OLH/templates/core/accounts/activate_account.html +++ b/src/themes/OLH/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 %} @@ -28,12 +29,17 @@

    {% trans "Activate Account" %}

    {% 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 %} - -{% 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/accounts/register.html b/src/themes/OLH/templates/core/accounts/register.html index 5a3d372c4f..3898ca57e5 100644 --- a/src/themes/OLH/templates/core/accounts/register.html +++ b/src/themes/OLH/templates/core/accounts/register.html @@ -30,7 +30,7 @@
    {% trans "Register for an account with" %} {{ request.press.name }}.

    {% blocktrans %}For more information read our password guide.{% endblocktrans %}

    - {% include "common/elements/orcid_registration.html" %} + {% include "common/elements/orcid_registration.html" with button_classes="button expanded orcid-button" %} {% include "elements/forms/errors.html" with form=form %} 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 @@

    {{ request.journal.name }}

    {% 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/clean/templates/core/accounts/orcid_registration.html b/src/themes/clean/templates/core/accounts/orcid_registration.html index 3e02269020..f69790fee1 100644 --- a/src/themes/clean/templates/core/accounts/orcid_registration.html +++ b/src/themes/clean/templates/core/accounts/orcid_registration.html @@ -1,22 +1,33 @@ {% extends "core/base.html" %} {% load foundation %} +{% 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 %}

    - -
    - -
    +
    + {% csrf_token %}
    -
    {% trans 'Login' %}
    +
    +

    {% trans "Unregistered ORCiD ID" %}

    +

    + {% blocktranslate %} + The ORCiD ID 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 ID for future use. + {% endblocktranslate %} +

    +

    + + {% trans 'Register' %} + +

    @@ -26,25 +37,21 @@
    {% trans 'Login' %}
    - -

    {% trans 'Forgot your password' %}?

    +

    + +

    +

    + + {% trans "Forgotten your password?" %} + +

    +
    - -
    - -
    - -
    -
    -
    {% trans 'Register' %}
    -
    - {% trans 'Register' %} -
    -
    -
    -{% endblock body %} \ No newline at end of file +{% endblock body %} diff --git a/src/themes/clean/templates/core/accounts/register.html b/src/themes/clean/templates/core/accounts/register.html index 2511eafdfd..be56f96d8d 100644 --- a/src/themes/clean/templates/core/accounts/register.html +++ b/src/themes/clean/templates/core/accounts/register.html @@ -20,7 +20,7 @@
    {% trans "Register for an account with" %} {{ request.press.name }}.
    {% include "common/elements/password_rules.html" %}

    {% blocktrans %}For more information read our password guide.{% endblocktrans %}

    - {% include "common/elements/orcid_registration.html" %} + {% include "common/elements/orcid_registration.html" with button_classes="btn orcid-button btn-block" %}
    {% bootstrap_form form %}

    diff --git a/src/themes/clean/templates/core/login.html b/src/themes/clean/templates/core/login.html index 2f008cd256..3a98bca212 100644 --- a/src/themes/clean/templates/core/login.html +++ b/src/themes/clean/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 %} @@ -12,13 +13,20 @@

    {% if settings.ENABLE_ORCID %}

    {% trans "Log in with your account" %}

    - {% trans "Log in with 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 %}
    {% include "common/elements/static-error-messages.html" with form=form %} @@ -40,12 +48,15 @@

    {% trans "Log in with your account" %}

    class="btn btn-primary btn-block">{% trans "Log in" %}

    -

    {% trans "Forgotten your password?" %} +

    + + {% trans "Forgotten your password?" %} +

    -

    - {% trans "Register a new account" %} + + {% trans "Register a new account" %} +

    diff --git a/src/themes/clean/templates/core/nav.html b/src/themes/clean/templates/core/nav.html index 82ee2712d5..498e10828f 100644 --- a/src/themes/clean/templates/core/nav.html +++ b/src/themes/clean/templates/core/nav.html @@ -100,8 +100,16 @@ {% else %} - - + + {% 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' %}

    {% if not request.user.is_authenticated %}{% trans 'Register' %} - {% trans 'Login' %}{% else %} + + {% trans 'Log in' %} + {% else %} {% trans 'Start Submission' %}{% endif %}

    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 %} - - + + {% 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 "Reset Your Password" %} + +

    +
    +
    +
    +
    +
    + +{% endblock body %} diff --git a/src/themes/material/templates/core/accounts/register.html b/src/themes/material/templates/core/accounts/register.html index 40c25933b7..9599b9345d 100644 --- a/src/themes/material/templates/core/accounts/register.html +++ b/src/themes/material/templates/core/accounts/register.html @@ -24,7 +24,7 @@

    {% blocktrans trimmed %}For more information read our password guide.{% endblocktrans %}

    - {% include "common/elements/orcid_registration.html" %} + {% include "common/elements/orcid_registration.html" with button_classes="btn wide-button orcid-button" %}
    {% include "elements/forms/errors.html" %} {% csrf_token %} 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 "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 %}