Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
efafcf3
save orcid tokens and expirations
everreau Jan 21, 2026
3d051e7
don't allow users to edit orcids directly in profile
everreau Jan 22, 2026
d2b237d
allow authors to request orcid from co-authors
everreau Jan 23, 2026
c1d4244
fix tests
everreau Jan 26, 2026
1250329
add tests
everreau Jan 26, 2026
7b90eb1
ruff formatting
everreau Jan 26, 2026
aba83d4
fix icon link, capitalize i per alainna
everreau Feb 6, 2026
90c8b1d
update tests
everreau Feb 6, 2026
0fc0f01
use ORCID iD (from Alainna)
everreau Feb 13, 2026
be0af16
show orcids in repository admin and allow requests
everreau Feb 13, 2026
c68057d
fix test
everreau Feb 13, 2026
641995c
allow different emails for inactive accounts
everreau Feb 13, 2026
1a6767d
add date orcid request was sent
everreau Feb 13, 2026
839d954
fix typos, minor changes
everreau Feb 17, 2026
e5e69e3
fix date format
everreau Feb 17, 2026
78e659e
use is_staff
everreau Feb 17, 2026
a4a866d
add comments, fix messages
everreau Feb 17, 2026
6a1023c
use url_with_return and require post
everreau Feb 18, 2026
e9bb6b4
don't use null=True for charfields, fix tests
everreau Feb 18, 2026
d80fbdc
ruff
everreau Feb 18, 2026
725bbbb
typo
everreau Feb 20, 2026
59803ef
use sandbox parameter when appropriate
everreau Feb 20, 2026
52fe5e8
re-arrange orcid field
everreau Feb 24, 2026
11cb04f
add orcid display to more view, add is_valid flag for orcids in froze…
everreau Mar 11, 2026
fd251e9
add tests for inactive accounts, re-write account matching logic to b…
everreau Mar 12, 2026
24a1d5d
ruff
everreau Mar 12, 2026
f0eb00a
add orcid authenticated flag to crossref metadata
everreau Mar 12, 2026
fc889aa
don't allow users to add an orcid that is already attached to another…
everreau Mar 12, 2026
a411892
remove unused code
everreau Mar 12, 2026
c27a70a
remove unused setting and update request text
everreau Mar 12, 2026
94357ae
use validation to determine which account to login to if there are du…
everreau Mar 13, 2026
f362c2b
make sure orcid display matches ENABLE_ORCID setting
everreau Mar 16, 2026
0ca9e28
don't let users register with or add orcids if an account with a vali…
everreau Mar 16, 2026
ab71ca6
ruff
everreau Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
29 changes: 29 additions & 0 deletions src/core/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,35 @@ def send_confirmation_link(request, new_user):
)


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}

user.date_orcid_requested = timezone.now()
user.save()

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,
template,
subject,
user.email,
context,
log_dict=log_dict,
)


def resize_and_crop(
img_path,
size=settings.DEFAULT_CROP_SIZE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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),
),
]
18 changes: 18 additions & 0 deletions src/core/migrations/0111_account_date_orcid_requested.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
12 changes: 12 additions & 0 deletions src/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -485,6 +486,9 @@ 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)
date_orcid_requested = models.DateTimeField(blank=True, null=True)
twitter = models.CharField(
max_length=300, null=True, blank=True, verbose_name=_("Twitter Handle")
)
Expand Down Expand Up @@ -948,6 +952,12 @@ 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)
Expand All @@ -959,6 +969,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)
Expand Down
84 changes: 81 additions & 3 deletions src/core/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -219,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,
Expand Down Expand Up @@ -252,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):
Expand Down Expand Up @@ -582,3 +583,80 @@ 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, '<input type="text" name="orcid" maxlength="40" id="id_orcid">'
)

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 iD 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, '<input type="hidden" name="orcid" value="0000-0000-0000-0000"/>'
)
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, '<input type="hidden" name="orcid" value="0000-0000-0000-0000"/>'
)
self.assertNotContains(response, "ORCID could not be validated.")
self.assertNotContains(response, "remove_orcid")
18 changes: 13 additions & 5 deletions src/core/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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 = "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]}
Expand All @@ -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 = "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 = {
Expand All @@ -471,7 +479,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,
Expand Down
Loading
Loading