From efafcf32fba72fb79d8d4dafc2c8d5826eac30eb Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Wed, 21 Jan 2026 16:17:07 -0600 Subject: [PATCH 01/13] save orcid tokens and expirations --- ...account_orcid_token_expiration_and_more.py | 33 +++++++++++++++++++ src/core/models.py | 10 ++++++ src/core/views.py | 11 +++++-- src/utils/orcid.py | 32 +++++++++++++++--- 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py diff --git a/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py b/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py new file mode 100644 index 0000000000..5bdea87253 --- /dev/null +++ b/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.26 on 2026-01-21 20:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0109_salutation_name_20250707_1420'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='orcid_token', + field=models.CharField(blank=True, max_length=40, null=True), + ), + migrations.AddField( + model_name='account', + name='orcid_token_expiration', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='orcidtoken', + name='access_token', + field=models.CharField(blank=True, max_length=40, null=True), + ), + migrations.AddField( + model_name='orcidtoken', + name='access_token_expiration', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index e02a2ab8b3..ae1816b802 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -71,6 +71,7 @@ from utils import logic as utils_logic from utils.forms import plain_text_validator from production import logic as production_logic +from utils.orcid import is_token_valid fs = JanewayFileSystemStorage() logger = get_logger(__name__) @@ -485,6 +486,8 @@ class Account(AbstractBaseUser, PermissionsMixin): orcid = models.CharField( max_length=40, null=True, blank=True, verbose_name=_("ORCiD") ) + orcid_token = models.CharField(max_length=40, null=True, blank=True) + orcid_token_expiration = models.DateTimeField(null=True, blank=True) twitter = models.CharField( max_length=300, null=True, blank=True, verbose_name=_("Twitter Handle") ) @@ -948,6 +951,11 @@ def hypothesis_username(self): )[:30] return username.lower() + def get_orcid_url(self): + return f"{settings.ORCID_URL.replace('oauth/authorize', '')}{self.orcid}" + + def is_orcid_token_valid(self): + return is_token_valid(self.orcid, self.orcid_token) def generate_expiry_date(): return timezone.now() + timedelta(days=1) @@ -959,6 +967,8 @@ class OrcidToken(models.Model): expiry = models.DateTimeField( default=generate_expiry_date, verbose_name=_("Expires on") ) + access_token = models.CharField(max_length=40, null=True, blank=True) + access_token_expiration = models.DateTimeField(null=True, blank=True) def __str__(self): return "ORCiD Token [{0}] - {1}".format(self.orcid, self.token) diff --git a/src/core/views.py b/src/core/views.py index 174c6a3712..69bafcb472 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -215,7 +215,7 @@ def user_login_orcid(request): # There is an orcid code, meaning the user has authenticated on orcid.org. # Make another request to orcid.org to verify it. - orcid_id = orcid.retrieve_tokens(orcid_code, request.site_type) + access_token, expiration, orcid_id = orcid.retrieve_tokens(orcid_code, request.site_type) # If verification did not work, send them to the regular login page. if not orcid_id: @@ -261,7 +261,11 @@ def user_login_orcid(request): # Then send the user to a decision page that tells them # the ORCID login did not work and they will need to register. models.OrcidToken.objects.filter(orcid=orcid_id).delete() - new_token = models.OrcidToken.objects.create(orcid=orcid_id) + new_token = models.OrcidToken.objects.create( + orcid=orcid_id, + access_token=access_token, + access_token_expiration=expiration + ) return redirect( logic.reverse_with_next( "core_orcid_registration", @@ -434,6 +438,9 @@ def register(request, orcid_token=None): if form.is_valid(): if token_obj: new_user = form.save() + new_user.orcid_token = token_obj.access_token + new_user.orcid_expiration = token_obj.access_token_expiration + new_user.save() if new_user.orcid: orcid_details = orcid.get_orcid_record_details(token_obj.orcid) for orcid_affil in orcid_details.get("affiliations", []): diff --git a/src/utils/orcid.py b/src/utils/orcid.py index d2d193bd94..7ec6bf9066 100755 --- a/src/utils/orcid.py +++ b/src/utils/orcid.py @@ -14,6 +14,7 @@ from django.http import QueryDict import requests from requests.exceptions import HTTPError +import datetime from utils import logic from utils.logger import get_logger @@ -48,13 +49,36 @@ def retrieve_tokens(authorization_code, site): r.raise_for_status() except HTTPError as e: logger.error("ORCID request failed: %s" % str(e)) - orcid_id = None + # after logging failure continue with an empty response + # to avoid additional errors + orcid_response = {} else: logger.info("OK response from ORCID") - orcid_id = json.loads(r.text).get("orcid") + orcid_response = json.loads(r.text) - return orcid_id + access_token = orcid_response.get('access_token', None) + orcid_id = orcid_response.get('orcid', None) + if 'expires_in' in orcid_response: + expires = orcid_response.get('expires_in') + expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=expires) + else: + expiration_date = None + + return access_token, expiration_date, orcid_id + +def is_token_valid(orcid_id, token): + api_client = OrcidAPI(settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True) + r = api_client._get_public_info(orcid_id, 'record', token, None, 'application/orcid+json') + return r.status_code == 200 + +def revoke_token(token): + url = settings.ORCID_TOKEN_URL.replace("token", "revoke") + data = {'client_id': settings.ORCID_CLIENT_ID, + 'client_secret': settings.ORCID_CLIENT_SECRET, + 'token': token} + r = requests.post(url, data=data) + return r.status_code == 200 def build_redirect_uri(site): """builds the landing page for ORCID requests @@ -67,7 +91,7 @@ def build_redirect_uri(site): def get_orcid_record(orcid): try: logger.info("Retrieving ORCID profile for %s", orcid) - api_client = OrcidAPI(settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET) + api_client = OrcidAPI(settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True) search_token = api_client.get_search_token_from_orcid() return api_client.read_record_public( orcid, From 3d051e7753896393a447e8c6201d5f54df5f222f Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Thu, 22 Jan 2026 15:19:09 -0600 Subject: [PATCH 02/13] don't allow users to edit orcids directly in profile --- src/core/admin.py | 7 ++++ src/core/views.py | 26 +++++++++++++ .../admin/elements/accounts/orcid_field.html | 38 +++++++++++++++++++ .../admin/elements/accounts/user_form.html | 9 ++++- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/templates/admin/elements/accounts/orcid_field.html diff --git a/src/core/admin.py b/src/core/admin.py index 740e7ae5ea..dbdd36a64e 100755 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -8,6 +8,7 @@ from django.contrib.auth.admin import UserAdmin from django.utils.safestring import mark_safe from django.template.defaultfilters import truncatewords +from django.conf import settings from utils import admin_utils from core import models, forms @@ -134,6 +135,12 @@ class AccountAdmin(UserAdmin): admin_utils.PasswordResetInline, ] + def get_readonly_fields(self, request, obj=None): + if settings.ENABLE_ORCID: + return ['orcid'] + else: + return [] + def _roles_in(self, obj): if obj: journals = journal_models.Journal.objects.filter( diff --git a/src/core/views.py b/src/core/views.py index 69bafcb472..0af5d8c95d 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -286,6 +286,26 @@ def user_login_orcid(request): kwargs={"orcid_token": str(new_token.token)}, ) ) + elif action == "add_profile_orcid": + if not request.user.is_authenticated: + messages.add_message( + request, + messages.WARNING, + _( + "You must be logged in to connect an ORCID to your account." + ), + ) + return redirect(logic.reverse_with_next("core_login", next_url)) + request.user.orcid = orcid_id + request.user.orcid_token = access_token + request.user.orcid_expiration = expiration + request.user.save() + messages.add_message( + request, + messages.SUCCESS, + _("Your ORCID has been connected to your account."), + ) + return redirect(logic.reverse_with_next("core_edit_profile", next_url)) @login_required @@ -552,6 +572,12 @@ def edit_profile(request): :return: HttpResponse object """ user = request.user + if 'remove_orcid' in request.GET: + if orcid.revoke_token(user.orcid_token): + user.orcid = None + user.orcid_token = None + user.save() + form = forms.EditAccountForm(instance=user) send_reader_notifications = False next_url = request.GET.get("next", "") diff --git a/src/templates/admin/elements/accounts/orcid_field.html b/src/templates/admin/elements/accounts/orcid_field.html new file mode 100644 index 0000000000..772db89f05 --- /dev/null +++ b/src/templates/admin/elements/accounts/orcid_field.html @@ -0,0 +1,38 @@ +{% load orcid %} +{% load static %} + +
+ + {% for error in form.orcid.errors %} +
+ + {{ error|escape }} +
+ {% endfor %} + {% if form.orcid.value %} + +

ORCID logo {{ form.instance.get_orcid_url }}

+ {% if form.instance.is_orcid_token_valid %} + {% if form.instance == request.user or request.user.is_admin %} +

Remove

+ {% endif %} + {% else %} +

ORCiD could not be validated.

+ {% endif %} + {% endif %} + {% if not form.orcid.value or not form.instance.is_orcid_token_valid %} + {% if form.instance == request.user %} + + Connect your ORCiD + + {% endif %} + {% endif %} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
diff --git a/src/templates/admin/elements/accounts/user_form.html b/src/templates/admin/elements/accounts/user_form.html index a0fdfdb389..42774ed9f9 100644 --- a/src/templates/admin/elements/accounts/user_form.html +++ b/src/templates/admin/elements/accounts/user_form.html @@ -14,11 +14,18 @@

{% trans "Social Media and Accounts" %}

{% include "admin/elements/forms/field.html" with field=form.twitter %} {% include "admin/elements/forms/field.html" with field=form.facebook %} - {% include "admin/elements/forms/field.html" with field=form.orcid %} {% include "admin/elements/forms/field.html" with field=form.github %} {% include "admin/elements/forms/field.html" with field=form.linkedin %} {% include "admin/elements/forms/field.html" with field=form.website %} + {% if not settings.ENABLE_ORCID %} + {% include "admin/elements/forms/field.html" with field=form.orcid %} + {% endif %}
+{% if settings.ENABLE_ORCID %} +
+ {% include 'admin/elements/accounts/orcid_field.html' with form=form %} +
+{% endif %}

{% trans "Biography and Signature" %}

From d2b237d44e81d2496c6999615ae68cd3560d3b4b Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 23 Jan 2026 15:31:41 -0600 Subject: [PATCH 03/13] allow authors to request orcid from co-authors --- src/core/logic.py | 16 +++++++ src/core/views.py | 45 +++++++++++-------- .../admin/elements/accounts/orcid_field.html | 16 ++++++- src/utils/install/journal_defaults.json | 38 ++++++++++++++++ 4 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/core/logic.py b/src/core/logic.py index 85479724e1..1facbf2300 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -147,6 +147,22 @@ def send_confirmation_link(request, new_user): log_dict=log_dict, ) +def send_orcid_request(request, user): + context = { + "user": user, + "user_profile_url": request.site_type.site_url( + reverse("core_edit_profile"), + ), + } + log_dict = {"level": "Info", "types": "ORCiD Request", "target": None} + notify_helpers.send_email_with_body_from_setting_template( + request, + "orcid_request", + "subject_orcid_request", + user.email, + context, + log_dict=log_dict, + ) def resize_and_crop( img_path, diff --git a/src/core/views.py b/src/core/views.py index 0af5d8c95d..c2df345f49 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -572,11 +572,6 @@ def edit_profile(request): :return: HttpResponse object """ user = request.user - if 'remove_orcid' in request.GET: - if orcid.revoke_token(user.orcid_token): - user.orcid = None - user.orcid_token = None - user.save() form = forms.EditAccountForm(instance=user) send_reader_notifications = False @@ -692,6 +687,12 @@ def edit_profile(request): elif "export" in request.POST: return logic.export_gdpr_user_profile(user) + elif "remove_orcid" in request.POST: + if orcid.revoke_token(user.orcid_token): + user.orcid = None + user.orcid_token = None + user.save() + form = forms.EditAccountForm(instance=user) template = "admin/core/accounts/edit_profile.html" context = { @@ -1567,24 +1568,32 @@ def user_edit(request, user_id): next_url = request.GET.get("next", "") if request.POST: - form = forms.EditAccountForm(request.POST, request.FILES, instance=user) - registration_form = forms.AdminUserForm( - request.POST, instance=user, request=request - ) - - if form.is_valid() and registration_form.is_valid(): - registration_form.save() - form.save() + if "request_orcid" in request.POST: + logic.send_orcid_request(request, user) messages.add_message( request, messages.SUCCESS, - "User account updated.", + _("Successfully requested ORCiD from user."), + ) + else: + form = forms.EditAccountForm(request.POST, request.FILES, instance=user) + registration_form = forms.AdminUserForm( + request.POST, instance=user, request=request ) - if next_url: - return redirect(next_url) - else: - return redirect(reverse("core_manager_users")) + if form.is_valid() and registration_form.is_valid(): + registration_form.save() + form.save() + messages.add_message( + request, + messages.SUCCESS, + "User account updated.", + ) + + if next_url: + return redirect(next_url) + else: + return redirect(reverse("core_manager_users")) template = "core/manager/users/edit.html" context = { diff --git a/src/templates/admin/elements/accounts/orcid_field.html b/src/templates/admin/elements/accounts/orcid_field.html index 772db89f05..00b77a03fe 100644 --- a/src/templates/admin/elements/accounts/orcid_field.html +++ b/src/templates/admin/elements/accounts/orcid_field.html @@ -15,10 +15,18 @@ {% endfor %} {% if form.orcid.value %} -

ORCID logo {{ form.instance.get_orcid_url }}

+

+ + ORCID logo {{ form.instance.get_orcid_url }} + +

{% if form.instance.is_orcid_token_valid %} {% if form.instance == request.user or request.user.is_admin %} -

Remove

+

+ +

{% endif %} {% else %}

ORCiD could not be validated.

@@ -30,6 +38,10 @@ class="button expanded orcid-button"> Connect your ORCiD + {% else %} + {% endif %} {% endif %} {% if field.help_text %} diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index ff88ba275a..cf4c24b361 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -1669,6 +1669,25 @@ "journal-manager" ] }, + { + "group": { + "name": "email" + }, + "setting": { + "description": "Email sent when user requests co-authors add ORCiD.", + "is_translatable": true, + "name": "orcid_request", + "pretty_name": "ORCiD Request", + "type": "rich-text" + }, + "value": { + "default": "

Dear {{ user.full_name }}

Your co-author has requested your ORCiD. You can add it by through your profile {{ user_profile_url }}.

" + }, + "editable_by": [ + "editor", + "journal-manager" + ] + }, { "group": { "name": "general" @@ -3607,6 +3626,25 @@ "journal-manager" ] }, + { + "value": { + "default": "ORCiD Request" + }, + "setting": { + "type": "char", + "pretty_name": "ORCiD Request", + "is_translatable": true, + "description": "Subject for when a submitter requests a co-author's ORCiD", + "name": "subject_orcid_request" + }, + "group": { + "name": "email_subject" + }, + "editable_by": [ + "editor", + "journal-manager" + ] + }, { "group": { "name": "email" From c1d424447ffb9846590c231f7c290e51c62af622 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Mon, 26 Jan 2026 11:10:29 -0600 Subject: [PATCH 04/13] fix tests --- src/core/tests/test_views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/tests/test_views.py b/src/core/tests/test_views.py index 3747d32fa5..769508f3ed 100644 --- a/src/core/tests/test_views.py +++ b/src/core/tests/test_views.py @@ -379,7 +379,7 @@ def test_no_orcid_code_redirects_with_next(self): @override_settings(URL_CONFIG="domain") @override_settings(ENABLE_ORCID=True) def test_no_orcid_id_redirects_with_next(self, retrieve_tokens): - retrieve_tokens.return_value = None + retrieve_tokens.return_value = None, None, None get_data = { "code": "12345", "next": self.next_url_raw, @@ -399,7 +399,7 @@ def test_action_login_account_found_redirects_to_next( self, retrieve_tokens, ): - retrieve_tokens.return_value = self.user_orcid_uri + retrieve_tokens.return_value = None, None, self.user_orcid_uri get_data = { "code": "12345", "next": self.next_url_raw, @@ -422,7 +422,7 @@ def test_action_login_matching_email_redirects_to_next( orcid_details, ): # Change ORCID so it doesn't work - retrieve_tokens.return_value = "https://orcid.org/0000-0001-2312-3123" + retrieve_tokens.return_value = None, None, "https://orcid.org/0000-0001-2312-3123" # Return an email that will work orcid_details.return_value = {"emails": [self.user_email]} @@ -449,7 +449,7 @@ def test_action_login_failure_redirects_with_next( orcid_details, ): # Change ORCID so it doesn't work - retrieve_tokens.return_value = "https://orcid.org/0000-0001-2312-3123" + retrieve_tokens.return_value = None, None, "https://orcid.org/0000-0001-2312-3123" orcid_details.return_value = {"emails": []} get_data = { @@ -471,7 +471,7 @@ def test_action_login_failure_redirects_with_next( @override_settings(URL_CONFIG="domain") @override_settings(ENABLE_ORCID=True) def test_action_register_redirects_with_next(self, retrieve_tokens): - retrieve_tokens.return_value = self.user_orcid_uri + retrieve_tokens.return_value = None, None, self.user_orcid_uri get_data = { "code": "12345", "next": self.next_url_raw, From 12503294e6ab1163999ec2026a07c8921d3a0375 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Mon, 26 Jan 2026 11:27:52 -0600 Subject: [PATCH 05/13] add tests --- src/core/tests/test_app.py | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/core/tests/test_app.py b/src/core/tests/test_app.py index 280f5a83cf..b4ea1adef9 100755 --- a/src/core/tests/test_app.py +++ b/src/core/tests/test_app.py @@ -15,6 +15,7 @@ from django.urls.base import clear_script_prefix from django.utils import timezone from django.core import mail +from journal.tests.utils import make_test_journal from utils.testing import helpers from utils import setting_handler, install @@ -582,3 +583,68 @@ def setUp(self): ) clear_script_prefix() + + @override_settings(ENABLE_ORCID=False) + def test_profile_orcid_disabled(self): + self.client.force_login(self.admin_user) + response = self.client.get(reverse('core_edit_profile')) + self.assertContains(response, '') + + def test_profile_orcid_enabled_no_orcid(self): + # Profile should offer to connect orcid + self.client.force_login(self.admin_user) + response = self.client.get(reverse('core_edit_profile')) + self.assertNotContains(response, "ORCiD could not be validated.") + self.assertContains(response, "Connect your ORCiD") + + @override_settings(ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + def test_profile_orcid_unverified(self): + self.admin_user.orcid = "0000-0000-0000-0000" + self.admin_user.save() + self.client.force_login(self.admin_user) + response = self.client.get(reverse('core_edit_profile')) + self.assertContains(response, "ORCiD could not be validated.") + self.assertContains(response, "Connect your ORCiD") + self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") + + @patch.object(models.Account, 'is_orcid_token_valid') + @override_settings(ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + def test_profile_orcid(self, mock_method): + # override is_orcid_token valid make if valid + mock_method.return_value = True + self.admin_user.orcid = "0000-0000-0000-0000" + self.admin_user.orcid_token = "0a0aaaaa-0aa0-0000-aa00-a00aa0a00000" + self.admin_user.save() + self.client.force_login(self.admin_user) + response = self.client.get(reverse('core_edit_profile')) + self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") + self.assertContains(response, "remove_orcid") + self.assertContains(response, '') + self.assertNotContains(response, "ORCiD could not be validated.") + + @patch.object(models.Account, 'is_orcid_token_valid') + @override_settings(URL_CONFIG="domain", ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + def test_profile_orcid_not_admin(self, mock_method): + mock_method.return_value = True + + journal_kwargs = {'code': "fetests", + 'domain': "fetests.janeway.systems",} + journal = make_test_journal(**journal_kwargs) + + journal_manager = helpers.create_user("jmanager@mailinator.com", ["journal-manager"], journal=journal) + journal_manager.is_active = True + journal_manager.save() + + self.regular_user.orcid = "0000-0000-0000-0000" + self.regular_user.orcid_token = "0a0aaaaa-0aa0-0000-aa00-a00aa0a00000" + self.regular_user.save() + + self.client.force_login(journal_manager) + + url = reverse('core_user_edit', kwargs={'user_id': self.regular_user.pk}) + response = self.client.get(url, SERVER_NAME=journal.domain) + + self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") + self.assertContains(response, '') + self.assertNotContains(response, "ORCiD could not be validated.") + self.assertNotContains(response, "remove_orcid") From 7b90eb106efabac12ca39266229c8353dd161eb2 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Mon, 26 Jan 2026 12:02:43 -0600 Subject: [PATCH 06/13] ruff formatting --- src/core/admin.py | 2 +- src/core/logic.py | 2 + ...account_orcid_token_expiration_and_more.py | 19 ++++---- src/core/models.py | 1 + src/core/tests/test_app.py | 44 ++++++++++++------- src/core/tests/test_views.py | 12 ++++- src/core/views.py | 10 ++--- src/utils/orcid.py | 31 ++++++++----- 8 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/core/admin.py b/src/core/admin.py index dbdd36a64e..60789723f4 100755 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -137,7 +137,7 @@ class AccountAdmin(UserAdmin): def get_readonly_fields(self, request, obj=None): if settings.ENABLE_ORCID: - return ['orcid'] + return ["orcid"] else: return [] diff --git a/src/core/logic.py b/src/core/logic.py index 1facbf2300..5f2014d574 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -147,6 +147,7 @@ def send_confirmation_link(request, new_user): log_dict=log_dict, ) + def send_orcid_request(request, user): context = { "user": user, @@ -164,6 +165,7 @@ def send_orcid_request(request, user): log_dict=log_dict, ) + def resize_and_crop( img_path, size=settings.DEFAULT_CROP_SIZE, diff --git a/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py b/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py index 5bdea87253..b4b3356419 100644 --- a/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py +++ b/src/core/migrations/0110_account_orcid_token_account_orcid_token_expiration_and_more.py @@ -4,30 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0109_salutation_name_20250707_1420'), + ("core", "0109_salutation_name_20250707_1420"), ] operations = [ migrations.AddField( - model_name='account', - name='orcid_token', + model_name="account", + name="orcid_token", field=models.CharField(blank=True, max_length=40, null=True), ), migrations.AddField( - model_name='account', - name='orcid_token_expiration', + model_name="account", + name="orcid_token_expiration", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='orcidtoken', - name='access_token', + model_name="orcidtoken", + name="access_token", field=models.CharField(blank=True, max_length=40, null=True), ), migrations.AddField( - model_name='orcidtoken', - name='access_token_expiration', + model_name="orcidtoken", + name="access_token_expiration", field=models.DateTimeField(blank=True, null=True), ), ] diff --git a/src/core/models.py b/src/core/models.py index ae1816b802..8cad9a76a6 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -957,6 +957,7 @@ def get_orcid_url(self): def is_orcid_token_valid(self): return is_token_valid(self.orcid, self.orcid_token) + def generate_expiry_date(): return timezone.now() + timedelta(days=1) diff --git a/src/core/tests/test_app.py b/src/core/tests/test_app.py index b4ea1adef9..eeef8e3ed9 100755 --- a/src/core/tests/test_app.py +++ b/src/core/tests/test_app.py @@ -587,28 +587,30 @@ def setUp(self): @override_settings(ENABLE_ORCID=False) def test_profile_orcid_disabled(self): self.client.force_login(self.admin_user) - response = self.client.get(reverse('core_edit_profile')) - self.assertContains(response, '') + response = self.client.get(reverse("core_edit_profile")) + self.assertContains( + response, '' + ) def test_profile_orcid_enabled_no_orcid(self): # Profile should offer to connect orcid self.client.force_login(self.admin_user) - response = self.client.get(reverse('core_edit_profile')) + response = self.client.get(reverse("core_edit_profile")) self.assertNotContains(response, "ORCiD could not be validated.") self.assertContains(response, "Connect your ORCiD") - @override_settings(ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + @override_settings(ORCID_URL="https://sandbox.orcid.org/oauth/authorize") def test_profile_orcid_unverified(self): self.admin_user.orcid = "0000-0000-0000-0000" self.admin_user.save() self.client.force_login(self.admin_user) - response = self.client.get(reverse('core_edit_profile')) + response = self.client.get(reverse("core_edit_profile")) self.assertContains(response, "ORCiD could not be validated.") self.assertContains(response, "Connect your ORCiD") self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") - @patch.object(models.Account, 'is_orcid_token_valid') - @override_settings(ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + @patch.object(models.Account, "is_orcid_token_valid") + @override_settings(ORCID_URL="https://sandbox.orcid.org/oauth/authorize") def test_profile_orcid(self, mock_method): # override is_orcid_token valid make if valid mock_method.return_value = True @@ -616,22 +618,30 @@ def test_profile_orcid(self, mock_method): self.admin_user.orcid_token = "0a0aaaaa-0aa0-0000-aa00-a00aa0a00000" self.admin_user.save() self.client.force_login(self.admin_user) - response = self.client.get(reverse('core_edit_profile')) + response = self.client.get(reverse("core_edit_profile")) self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") self.assertContains(response, "remove_orcid") - self.assertContains(response, '') + self.assertContains( + response, '' + ) self.assertNotContains(response, "ORCiD could not be validated.") - @patch.object(models.Account, 'is_orcid_token_valid') - @override_settings(URL_CONFIG="domain", ORCID_URL='https://sandbox.orcid.org/oauth/authorize') + @patch.object(models.Account, "is_orcid_token_valid") + @override_settings( + URL_CONFIG="domain", ORCID_URL="https://sandbox.orcid.org/oauth/authorize" + ) def test_profile_orcid_not_admin(self, mock_method): mock_method.return_value = True - journal_kwargs = {'code': "fetests", - 'domain': "fetests.janeway.systems",} + journal_kwargs = { + "code": "fetests", + "domain": "fetests.janeway.systems", + } journal = make_test_journal(**journal_kwargs) - journal_manager = helpers.create_user("jmanager@mailinator.com", ["journal-manager"], journal=journal) + journal_manager = helpers.create_user( + "jmanager@mailinator.com", ["journal-manager"], journal=journal + ) journal_manager.is_active = True journal_manager.save() @@ -641,10 +651,12 @@ def test_profile_orcid_not_admin(self, mock_method): self.client.force_login(journal_manager) - url = reverse('core_user_edit', kwargs={'user_id': self.regular_user.pk}) + url = reverse("core_user_edit", kwargs={"user_id": self.regular_user.pk}) response = self.client.get(url, SERVER_NAME=journal.domain) self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") - self.assertContains(response, '') + self.assertContains( + response, '' + ) self.assertNotContains(response, "ORCiD could not be validated.") self.assertNotContains(response, "remove_orcid") diff --git a/src/core/tests/test_views.py b/src/core/tests/test_views.py index 769508f3ed..2f33ccd85f 100644 --- a/src/core/tests/test_views.py +++ b/src/core/tests/test_views.py @@ -422,7 +422,11 @@ def test_action_login_matching_email_redirects_to_next( orcid_details, ): # Change ORCID so it doesn't work - retrieve_tokens.return_value = None, None, "https://orcid.org/0000-0001-2312-3123" + retrieve_tokens.return_value = ( + None, + None, + "https://orcid.org/0000-0001-2312-3123", + ) # Return an email that will work orcid_details.return_value = {"emails": [self.user_email]} @@ -449,7 +453,11 @@ def test_action_login_failure_redirects_with_next( orcid_details, ): # Change ORCID so it doesn't work - retrieve_tokens.return_value = None, None, "https://orcid.org/0000-0001-2312-3123" + retrieve_tokens.return_value = ( + None, + None, + "https://orcid.org/0000-0001-2312-3123", + ) orcid_details.return_value = {"emails": []} get_data = { diff --git a/src/core/views.py b/src/core/views.py index c2df345f49..f49e4dfd80 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -215,7 +215,9 @@ def user_login_orcid(request): # There is an orcid code, meaning the user has authenticated on orcid.org. # Make another request to orcid.org to verify it. - access_token, expiration, orcid_id = orcid.retrieve_tokens(orcid_code, request.site_type) + access_token, expiration, orcid_id = orcid.retrieve_tokens( + orcid_code, request.site_type + ) # If verification did not work, send them to the regular login page. if not orcid_id: @@ -264,7 +266,7 @@ def user_login_orcid(request): new_token = models.OrcidToken.objects.create( orcid=orcid_id, access_token=access_token, - access_token_expiration=expiration + access_token_expiration=expiration, ) return redirect( logic.reverse_with_next( @@ -291,9 +293,7 @@ def user_login_orcid(request): messages.add_message( request, messages.WARNING, - _( - "You must be logged in to connect an ORCID to your account." - ), + _("You must be logged in to connect an ORCID to your account."), ) return redirect(logic.reverse_with_next("core_login", next_url)) request.user.orcid = orcid_id diff --git a/src/utils/orcid.py b/src/utils/orcid.py index 7ec6bf9066..63ae02ac28 100755 --- a/src/utils/orcid.py +++ b/src/utils/orcid.py @@ -56,30 +56,39 @@ def retrieve_tokens(authorization_code, site): logger.info("OK response from ORCID") orcid_response = json.loads(r.text) - access_token = orcid_response.get('access_token', None) - orcid_id = orcid_response.get('orcid', None) + access_token = orcid_response.get("access_token", None) + orcid_id = orcid_response.get("orcid", None) - if 'expires_in' in orcid_response: - expires = orcid_response.get('expires_in') + if "expires_in" in orcid_response: + expires = orcid_response.get("expires_in") expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=expires) else: expiration_date = None return access_token, expiration_date, orcid_id + def is_token_valid(orcid_id, token): - api_client = OrcidAPI(settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True) - r = api_client._get_public_info(orcid_id, 'record', token, None, 'application/orcid+json') + api_client = OrcidAPI( + settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True + ) + r = api_client._get_public_info( + orcid_id, "record", token, None, "application/orcid+json" + ) return r.status_code == 200 + def revoke_token(token): url = settings.ORCID_TOKEN_URL.replace("token", "revoke") - data = {'client_id': settings.ORCID_CLIENT_ID, - 'client_secret': settings.ORCID_CLIENT_SECRET, - 'token': token} + data = { + "client_id": settings.ORCID_CLIENT_ID, + "client_secret": settings.ORCID_CLIENT_SECRET, + "token": token, + } r = requests.post(url, data=data) return r.status_code == 200 + def build_redirect_uri(site): """builds the landing page for ORCID requests :site: Object implementing the AbstractSiteModel interface @@ -91,7 +100,9 @@ def build_redirect_uri(site): def get_orcid_record(orcid): try: logger.info("Retrieving ORCID profile for %s", orcid) - api_client = OrcidAPI(settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True) + api_client = OrcidAPI( + settings.ORCID_CLIENT_ID, settings.ORCID_CLIENT_SECRET, sandbox=True + ) search_token = api_client.get_search_token_from_orcid() return api_client.read_record_public( orcid, From aba83d461c75e0638df85fb18ebfa2371375bae6 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 6 Feb 2026 16:03:25 -0600 Subject: [PATCH 07/13] fix icon link, capitalize i per alainna --- src/templates/admin/elements/accounts/orcid_field.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/templates/admin/elements/accounts/orcid_field.html b/src/templates/admin/elements/accounts/orcid_field.html index 00b77a03fe..b98f10ba87 100644 --- a/src/templates/admin/elements/accounts/orcid_field.html +++ b/src/templates/admin/elements/accounts/orcid_field.html @@ -17,7 +17,7 @@

- ORCID logo {{ form.instance.get_orcid_url }} + ORCID logo {{ form.instance.get_orcid_url }}

{% if form.instance.is_orcid_token_valid %} @@ -29,18 +29,18 @@

{% endif %} {% else %} -

ORCiD could not be validated.

+

ORCID could not be validated.

{% endif %} {% endif %} {% if not form.orcid.value or not form.instance.is_orcid_token_valid %} {% if form.instance == request.user %} - Connect your ORCiD + Connect your ORCID {% else %} {% endif %} {% endif %} From 90c8b1df43a8d87e89934d1f4f5abc85b24281c4 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 6 Feb 2026 16:13:05 -0600 Subject: [PATCH 08/13] update tests --- src/core/tests/test_app.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/tests/test_app.py b/src/core/tests/test_app.py index eeef8e3ed9..a496271e26 100755 --- a/src/core/tests/test_app.py +++ b/src/core/tests/test_app.py @@ -220,7 +220,7 @@ def test_register_with_orcid_token(self, record_mock): self.assertContains(response, "Campbell") self.assertContains(response, "Kasey") self.assertContains(response, "campbell@evu.edu") - self.assertNotContains(response, "Register with ORCiD") + self.assertNotContains(response, "Register with ORCID") self.assertContains(response, "http://sandbox.orcid.org/0000-0000-0000-0000") self.assertContains( response, @@ -253,13 +253,13 @@ def test_register_with_orcid_token(self, record_mock): def test_registration(self): response = self.client.get(reverse("core_register")) self.assertEqual(response.status_code, 200) - self.assertContains(response, "Register with ORCiD") + self.assertContains(response, "Register with ORCID") @override_settings(ENABLE_ORCID=False) def test_registration(self): response = self.client.get(reverse("core_register")) self.assertEqual(response.status_code, 200) - self.assertNotContains(response, "Register with ORCiD") + self.assertNotContains(response, "Register with ORCID") @override_settings(URL_CONFIG="domain", CAPTCHA_TYPE=None) def test_mixed_case_login_different_case(self): @@ -596,8 +596,8 @@ def test_profile_orcid_enabled_no_orcid(self): # Profile should offer to connect orcid self.client.force_login(self.admin_user) response = self.client.get(reverse("core_edit_profile")) - self.assertNotContains(response, "ORCiD could not be validated.") - self.assertContains(response, "Connect your ORCiD") + self.assertNotContains(response, "ORCID could not be validated.") + self.assertContains(response, "Connect your ORCID") @override_settings(ORCID_URL="https://sandbox.orcid.org/oauth/authorize") def test_profile_orcid_unverified(self): @@ -605,8 +605,8 @@ def test_profile_orcid_unverified(self): self.admin_user.save() self.client.force_login(self.admin_user) response = self.client.get(reverse("core_edit_profile")) - self.assertContains(response, "ORCiD could not be validated.") - self.assertContains(response, "Connect your ORCiD") + self.assertContains(response, "ORCID could not be validated.") + self.assertContains(response, "Connect your ORCID") self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") @patch.object(models.Account, "is_orcid_token_valid") @@ -624,7 +624,7 @@ def test_profile_orcid(self, mock_method): self.assertContains( response, '' ) - self.assertNotContains(response, "ORCiD could not be validated.") + self.assertNotContains(response, "ORCID could not be validated.") @patch.object(models.Account, "is_orcid_token_valid") @override_settings( @@ -658,5 +658,5 @@ def test_profile_orcid_not_admin(self, mock_method): self.assertContains( response, '' ) - self.assertNotContains(response, "ORCiD could not be validated.") + self.assertNotContains(response, "ORCID could not be validated.") self.assertNotContains(response, "remove_orcid") From 0fc0f01e21a3ace28badb2b9012d2da9007a5e22 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 13 Feb 2026 11:38:39 -0600 Subject: [PATCH 09/13] use ORCID iD (from Alainna) --- .../admin/elements/accounts/orcid_field.html | 6 ++-- src/utils/install/journal_defaults.json | 31 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/templates/admin/elements/accounts/orcid_field.html b/src/templates/admin/elements/accounts/orcid_field.html index b98f10ba87..4574b61c04 100644 --- a/src/templates/admin/elements/accounts/orcid_field.html +++ b/src/templates/admin/elements/accounts/orcid_field.html @@ -29,18 +29,18 @@

{% endif %} {% else %} -

ORCID could not be validated.

+

ORCID iD could not be validated.

{% endif %} {% endif %} {% if not form.orcid.value or not form.instance.is_orcid_token_valid %} {% if form.instance == request.user %} - Connect your ORCID + Connect your ORCID iD {% else %} {% endif %} {% endif %} diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index cf4c24b361..c6d073b53e 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -1674,14 +1674,33 @@ "name": "email" }, "setting": { - "description": "Email sent when user requests co-authors add ORCiD.", + "description": "Email sent when user requests co-authors add ORCID iD.", "is_translatable": true, "name": "orcid_request", - "pretty_name": "ORCiD Request", + "pretty_name": "ORCID iD Request", "type": "rich-text" }, "value": { - "default": "

Dear {{ user.full_name }}

Your co-author has requested your ORCiD. You can add it by through your profile {{ user_profile_url }}.

" + "default": "

Dear {{ user.full_name }}

Your co-author has requested your ORCID iD. You can add it by through your profile {{ user_profile_url }}.

" + }, + "editable_by": [ + "editor", + "journal-manager" + ] + }, + { + "group": { + "name": "email" + }, + "setting": { + "description": "Email sent to co-authors that do not have accounts to create an account and add ORCID iD.", + "is_translatable": true, + "name": "orcid_create_request", + "pretty_name": "ORCID iD Create Request", + "type": "rich-text" + }, + "value": { + "default": "

Dear {{ user.full_name }}

Your co-author has requested your ORCID iD. You can add it by through your profile {{ user_profile_url }}.

" }, "editable_by": [ "editor", @@ -3628,13 +3647,13 @@ }, { "value": { - "default": "ORCiD Request" + "default": "ORCID iD Request" }, "setting": { "type": "char", - "pretty_name": "ORCiD Request", + "pretty_name": "ORCID iD Request", "is_translatable": true, - "description": "Subject for when a submitter requests a co-author's ORCiD", + "description": "Subject for when a submitter requests a co-author's ORCID iD", "name": "subject_orcid_request" }, "group": { From be0af16deb7f02d2474670f3f10c7f35326b8a5b Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 13 Feb 2026 11:43:07 -0600 Subject: [PATCH 10/13] show orcids in repository admin and allow requests --- src/repository/urls.py | 6 +++++ src/repository/views.py | 23 +++++++++++++++++++ src/templates/admin/repository/article.html | 10 ++++++++ .../admin/repository/submit/authors.html | 10 ++++++++ .../admin/repository/submit/review.html | 10 ++++++++ 5 files changed, 59 insertions(+) diff --git a/src/repository/urls.py b/src/repository/urls.py index f8251dd254..8bf5fb4623 100755 --- a/src/repository/urls.py +++ b/src/repository/urls.py @@ -68,6 +68,12 @@ views.repository_delete_author, name="repository_delete_author", ), + re_path( + r"^manager/(?P\d+)/author/(?P\d+)/request_orcid/(?P[-\w]+)/$", + views.repository_request_orcid, + name="repository_request_orcid", + ), + re_path( r"^submit/(?P\d+)/authors/order/$", views.preprints_author_order, diff --git a/src/repository/views.py b/src/repository/views.py index 82c7015219..5134983dad 100644 --- a/src/repository/views.py +++ b/src/repository/views.py @@ -27,6 +27,7 @@ models as core_models, forms as core_forms, views as core_views, + logic as core_logic, ) from journal import models as journal_models from utils import ( @@ -1466,6 +1467,28 @@ def preprints_author_order(request, preprint_id): return HttpResponse("Complete") +@login_required +def repository_request_orcid(request, preprint_id, account_id, redirect_path): + user = get_object_or_404( + core_models.Account, + pk=account_id, + ) + core_logic.send_orcid_request(request, user) + messages.add_message( + request, + messages.SUCCESS, + f"Successfully requested ORCID from {user.full_name()}", + ) + + return redirect( + reverse( + redirect_path, + kwargs={ + "preprint_id": preprint_id, + }, + ) + ) + @login_required @require_POST diff --git a/src/templates/admin/repository/article.html b/src/templates/admin/repository/article.html index ce851b814b..7a7bb5d0f4 100644 --- a/src/templates/admin/repository/article.html +++ b/src/templates/admin/repository/article.html @@ -227,6 +227,7 @@

Authors

{% trans 'Name' %} {% trans 'Email' %} + {% trans 'ORCID' %} {% trans 'Affiliation' %} {% trans 'Edit' %} {% trans 'Delete' %} @@ -237,6 +238,15 @@

Authors

{{ author.account.full_name }} {{ author.account.email }} + + {% if author.account.orcid %} + {{ author.account.orcid }} + {% else %} + + Request + + {% endif %} + {% if author.affiliation %}{{ author.affiliation }}{% else %} {{ author.account.institution }}{% endif %} diff --git a/src/templates/admin/repository/submit/authors.html b/src/templates/admin/repository/submit/authors.html index 6d256c22d9..2a64491b83 100644 --- a/src/templates/admin/repository/submit/authors.html +++ b/src/templates/admin/repository/submit/authors.html @@ -75,6 +75,7 @@

{% trans 'Authors' %}

{% trans 'Name' %} {% trans 'Email' %} + {% trans 'ORCID' %} {% trans 'Affiliation' %} {% trans 'Delete' %} @@ -84,6 +85,15 @@

{% trans 'Authors' %}

{{ author.account.full_name }} {{ author.account.email }} + + {% if author.account.orcid %} + {{ author.account.orcid }} + {% else %} + + Request + + {% endif %} + {{ author.display_affiliation }} diff --git a/src/templates/admin/repository/submit/review.html b/src/templates/admin/repository/submit/review.html index f4dbd2ce29..aec84ad981 100644 --- a/src/templates/admin/repository/submit/review.html +++ b/src/templates/admin/repository/submit/review.html @@ -55,12 +55,22 @@

Authors

Email Address First Name Last Name + ORCID {% for author in preprint.preprintauthor_set.all %} {{ author.account.email }} {{ author.account.first_name }} {{ author.account.last_name }} + + {% if author.account.orcid %} + {{ author.account.orcid }} + {% else %} + + Request + + {% endif %} + {% endfor %} From c68057d2ba3ee0c15a6e273fb3477844f0a331a9 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 13 Feb 2026 12:08:53 -0600 Subject: [PATCH 11/13] fix test --- src/core/tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/tests/test_app.py b/src/core/tests/test_app.py index a496271e26..cd14d93a09 100755 --- a/src/core/tests/test_app.py +++ b/src/core/tests/test_app.py @@ -605,7 +605,7 @@ def test_profile_orcid_unverified(self): self.admin_user.save() self.client.force_login(self.admin_user) response = self.client.get(reverse("core_edit_profile")) - self.assertContains(response, "ORCID could not be validated.") + self.assertContains(response, "ORCID iD could not be validated.") self.assertContains(response, "Connect your ORCID") self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000") From 641995c6bbb1b8416295bb70d3e518f8dc52c51a Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 13 Feb 2026 12:09:46 -0600 Subject: [PATCH 12/13] allow different emails for inactive accounts --- src/core/logic.py | 14 +++++++++++--- src/utils/install/journal_defaults.json | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/core/logic.py b/src/core/logic.py index 5f2014d574..e8994a5f21 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -155,11 +155,19 @@ def send_orcid_request(request, user): reverse("core_edit_profile"), ), } - log_dict = {"level": "Info", "types": "ORCiD Request", "target": None} + log_dict = {"level": "Info", "types": "ORCID Request", "target": None} + + if user.is_active: + template = "orcid_request" + subject = "subject_orcid_request" + else: + template = "orcid_activate_request" + subject = "subject_orcid_activate_request" + notify_helpers.send_email_with_body_from_setting_template( request, - "orcid_request", - "subject_orcid_request", + template, + subject, user.email, context, log_dict=log_dict, diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index c6d073b53e..29e464730d 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -1695,7 +1695,7 @@ "setting": { "description": "Email sent to co-authors that do not have accounts to create an account and add ORCID iD.", "is_translatable": true, - "name": "orcid_create_request", + "name": "orcid_activate_request", "pretty_name": "ORCID iD Create Request", "type": "rich-text" }, @@ -3664,6 +3664,25 @@ "journal-manager" ] }, + { + "value": { + "default": "ORCID iD Request" + }, + "setting": { + "type": "char", + "pretty_name": "ORCID iD Request", + "is_translatable": true, + "description": "Subject for when a submitter requests a co-author's ORCID iD and their account is inactive", + "name": "subject_orcid_activate_request" + }, + "group": { + "name": "email_subject" + }, + "editable_by": [ + "editor", + "journal-manager" + ] + }, { "group": { "name": "email" From 1a6767d98bc5632314a4bc9fb87cb4000c464287 Mon Sep 17 00:00:00 2001 From: Esther Verreau Date: Fri, 13 Feb 2026 14:14:51 -0600 Subject: [PATCH 13/13] add date orcid request was sent --- src/core/logic.py | 3 +++ .../0111_account_date_orcid_requested.py | 18 ++++++++++++++++++ src/core/models.py | 1 + src/core/views.py | 2 +- .../admin/elements/accounts/orcid_field.html | 3 +++ .../admin/elements/repository/orcid.html | 10 ++++++++++ src/templates/admin/repository/article.html | 8 +------- .../admin/repository/submit/authors.html | 8 +------- .../admin/repository/submit/review.html | 8 +------- 9 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 src/core/migrations/0111_account_date_orcid_requested.py create mode 100644 src/templates/admin/elements/repository/orcid.html diff --git a/src/core/logic.py b/src/core/logic.py index e8994a5f21..5c9a54f65f 100755 --- a/src/core/logic.py +++ b/src/core/logic.py @@ -157,6 +157,9 @@ def send_orcid_request(request, user): } log_dict = {"level": "Info", "types": "ORCID Request", "target": None} + user.date_orcid_requested = timezone.now() + user.save() + if user.is_active: template = "orcid_request" subject = "subject_orcid_request" diff --git a/src/core/migrations/0111_account_date_orcid_requested.py b/src/core/migrations/0111_account_date_orcid_requested.py new file mode 100644 index 0000000000..65b6d50922 --- /dev/null +++ b/src/core/migrations/0111_account_date_orcid_requested.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.26 on 2026-02-13 18:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0110_account_orcid_token_account_orcid_token_expiration_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='date_orcid_requested', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 8cad9a76a6..2c63b3a977 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -488,6 +488,7 @@ class Account(AbstractBaseUser, PermissionsMixin): ) orcid_token = models.CharField(max_length=40, null=True, blank=True) orcid_token_expiration = models.DateTimeField(null=True, blank=True) + date_orcid_requested = models.DateTimeField(blank=True, null=True) twitter = models.CharField( max_length=300, null=True, blank=True, verbose_name=_("Twitter Handle") ) diff --git a/src/core/views.py b/src/core/views.py index f49e4dfd80..68aa31d8c8 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -1573,7 +1573,7 @@ def user_edit(request, user_id): messages.add_message( request, messages.SUCCESS, - _("Successfully requested ORCiD from user."), + _("Successfully requested ORCID iD from user."), ) else: form = forms.EditAccountForm(request.POST, request.FILES, instance=user) diff --git a/src/templates/admin/elements/accounts/orcid_field.html b/src/templates/admin/elements/accounts/orcid_field.html index 4574b61c04..fe631e79f0 100644 --- a/src/templates/admin/elements/accounts/orcid_field.html +++ b/src/templates/admin/elements/accounts/orcid_field.html @@ -39,6 +39,9 @@ Connect your ORCID iD {% else %} + {% if form.instance.date_orcid_requested %} +

Request sent: {{ form.instance.date_orcid_requested|date:"m/d/Y" }}

+ {% endif %} diff --git a/src/templates/admin/elements/repository/orcid.html b/src/templates/admin/elements/repository/orcid.html new file mode 100644 index 0000000000..05d736d743 --- /dev/null +++ b/src/templates/admin/elements/repository/orcid.html @@ -0,0 +1,10 @@ +{% if author.account.orcid %} + {{ author.account.orcid }} +{% else %} + {% if author.account.date_orcid_requested %} +

Request sent: {{ author.account.date_orcid_requested|date:"m/d/Y" }}

+ {% endif %} + + Request + +{% endif %} diff --git a/src/templates/admin/repository/article.html b/src/templates/admin/repository/article.html index 7a7bb5d0f4..49d892fb04 100644 --- a/src/templates/admin/repository/article.html +++ b/src/templates/admin/repository/article.html @@ -239,13 +239,7 @@

Authors

{{ author.account.full_name }} {{ author.account.email }} - {% if author.account.orcid %} - {{ author.account.orcid }} - {% else %} - - Request - - {% endif %} + {% include "admin/elements/repository/orcid.html" %} {% if author.affiliation %}{{ author.affiliation }}{% else %} {{ author.account.institution }}{% endif %} diff --git a/src/templates/admin/repository/submit/authors.html b/src/templates/admin/repository/submit/authors.html index 2a64491b83..3465816ce0 100644 --- a/src/templates/admin/repository/submit/authors.html +++ b/src/templates/admin/repository/submit/authors.html @@ -86,13 +86,7 @@

{% trans 'Authors' %}

{{ author.account.full_name }} {{ author.account.email }} - {% if author.account.orcid %} - {{ author.account.orcid }} - {% else %} - - Request - - {% endif %} + {% include "admin/elements/repository/orcid.html" %} {{ author.display_affiliation }} diff --git a/src/templates/admin/repository/submit/review.html b/src/templates/admin/repository/submit/review.html index aec84ad981..0f47d44798 100644 --- a/src/templates/admin/repository/submit/review.html +++ b/src/templates/admin/repository/submit/review.html @@ -63,13 +63,7 @@

Authors

{{ author.account.first_name }} {{ author.account.last_name }} - {% if author.account.orcid %} - {{ author.account.orcid }} - {% else %} - - Request - - {% endif %} + {% include "admin/elements/repository/orcid.html" %} {% endfor %}