Skip to content

Commit f0b7d2b

Browse files
authored
Support for the Research Organization Registry (ROR) (#4483)
* First draft of data model #3168 * Build ROR importer #3168 * Updates to data model #3168 * Create standard buttons #3168 * UI for core affiliation management #3168 * Deprecate several more old account templates #4380 * chore #3168 Clean up old prints in testing * Work through regressions #3168 * Avoid Django ListView paginator bug * Display primary affiliation; add primary if first #3168 * feat #3168 Add affiliation inline to admin pages * feat #3168 Migrations for existing data * feat #3168 Avoid bug that caused duplicate locations to be created * feat #3168 Add country and location to data migration * feat #3168 Override get_or_create to handle old affiliation fields * feat #3168 Utility for matching legacy organizations * feat #3168 Improve importing process * feat #3168 Fix a bug where a bool might be unset * feat #3168 Fix a bug with a URL name * feat #3168 Display start and end dates on affiliations * feat #3168 Use function-based views for CRUD interface * feat #3168 Fix logic that manages affiliations * Clean up submission test data * feat #3168 Adjustments from manual testing * feat #3168 Expand max_length of OrganizationName.value * feat #3168 Validate exclusive fields on Affiliation * feat #3168 Fix view bugs and write tests * feat #3168 Make OrganizationName.value a required field * feat #3168 Fixes to backwards compatibility * feat #3168 Reflow migrations after rebase * feat #3168 Make account affiliation form more secure * feat #3168 Pass account to form kwargs * feat #3168 Cleanup requested in review * feat #3168 Include variable names in translatable strings * feat #3168 Use safer method in affil create view * feat #3168 Guard against duplicate affiliations * feat #3168 Fix bugs * feat #3168 Rename methods * feat #3168 Prepend 'is_' to status-related properties * feat #3168 Log exceptions not just errors * feat #3168 Refactor exclusive fields check as model constraint * feat #3168 Account for case where all exclusive fields are blank * feat #3168 Store non-URI form of ROR ID * feat #3168 More small fixes * feat #3168 Reflow migrations again * feat #3168 Remove accidentally added migration * feat #3168 Change print statement * feat #3168 Check defaults dict in backwards compat method * feat #3168 Backwards compatibility in queryset lookups * feat #3168 Backwards compatibility in queryset lookups * feat #3168 Improve regex handling * feat #3168 Improve logging * feat #3168 Avoid unnecessary change to test * feat #3168 Delay importing latitude and longitude * feat #3168 Fix bug between ISO-639-1 and ISO-639-2/T * feat #3168 Document odd Account.objects.create custom method * feat #3168 Move regex patterns to constant
1 parent 1df1b31 commit f0b7d2b

File tree

64 files changed

+3662
-148
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+3662
-148
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ geoip2==4.8.0
3131
html2text==2017.10.4
3232
idna==2.7
3333
ipaddress==1.0.16
34+
iso639-lang==2.6
3435
django-ipware==7.0.1
3536
more-itertools==10.1.0
3637
jsmin==3.0.1

src/core/admin.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ class SettingAdmin(admin.ModelAdmin):
4040
class AccountAdmin(UserAdmin):
4141
"""Displays Account objects in the Django admin interface."""
4242
list_display = ('id', 'email', 'orcid', 'first_name', 'middle_name',
43-
'last_name', 'institution', '_roles_in', 'last_login')
43+
'last_name', '_roles_in', 'last_login')
4444
list_display_links = ('id', 'email')
4545
list_filter = ('accountrole__journal',
4646
'repositoryrole__repository__short_name',
4747
'is_active', 'is_staff', 'is_admin', 'is_superuser',
4848
'last_login')
4949
search_fields = ('id', 'username', 'email', 'first_name', 'middle_name',
50-
'last_name', 'orcid', 'institution',
50+
'last_name', 'orcid',
5151
'biography', 'signature')
5252

5353
fieldsets = UserAdmin.fieldsets + (
5454
(None, {'fields': (
5555
'name_prefix', 'middle_name', 'orcid',
56-
'institution', 'department', 'country', 'twitter',
56+
'twitter',
5757
'linkedin', 'facebook', 'github', 'website', 'biography', 'enable_public_profile',
5858
'signature', 'profile_image', 'interest', "preferred_timezone",
5959
)}),
@@ -71,6 +71,7 @@ class AccountAdmin(UserAdmin):
7171
raw_id_fields = ('interest',)
7272

7373
inlines = [
74+
admin_utils.ControlledAffiliationInline,
7475
admin_utils.AccountRoleInline,
7576
admin_utils.RepositoryRoleInline,
7677
admin_utils.EditorialGroupMemberInline,
@@ -398,6 +399,84 @@ class AccessRequestAdmin(admin.ModelAdmin):
398399
date_hierarchy = ('requested')
399400

400401

402+
class OrganizationAdmin(admin.ModelAdmin):
403+
list_display = ('pk', 'ror_id', '_ror_display', '_custom_label',
404+
'website', '_locations', 'ror_status')
405+
list_display_links = ('pk', 'ror_id')
406+
list_filter = ('ror_status', 'locations__country')
407+
search_fields = ('pk', 'ror_display__value', 'custom_label__value', 'labels__value',
408+
'aliases__value', 'acronyms__value', 'website', 'ror_id')
409+
raw_id_fields = ('locations', )
410+
411+
def _ror_display(self, obj):
412+
return obj.ror_display if obj and obj.ror_display else ''
413+
414+
def _locations(self, obj):
415+
return '; '.join([str(l) for l in obj.locations.all()]) if obj else ''
416+
417+
def _custom_label(self, obj):
418+
return obj.custom_label if obj and obj.custom_label else ''
419+
420+
421+
class OrganizationNameAdmin(admin.ModelAdmin):
422+
list_display = ('pk', 'value', 'language')
423+
list_display_links = ('pk', 'value')
424+
search_fields = ('pk', 'value')
425+
raw_id_fields = ('ror_display_for', 'custom_label_for',
426+
'label_for', 'alias_for', 'acronym_for')
427+
428+
def _ror_display(self, obj):
429+
return obj.ror_display if obj and obj.ror_display else ''
430+
431+
def _locations(self, obj):
432+
return '; '.join([str(l) for l in obj.locations.all()]) if obj else ''
433+
434+
def _custom_label(self, obj):
435+
return obj.custom_label if obj and obj.custom_label else ''
436+
437+
438+
class LocationAdmin(admin.ModelAdmin):
439+
list_display = ('pk', 'name', 'country', 'geonames_id')
440+
list_display_links = ('pk', 'name')
441+
list_filter = ('country',)
442+
search_fields = ('pk', 'name', 'country__code', 'country__name',
443+
'geonames_id')
444+
445+
446+
class ControlledAffiliationAdmin(admin.ModelAdmin):
447+
list_display = ('pk', '_person', 'organization',
448+
'title', 'department', 'start', 'end')
449+
list_display_links = ('pk', '_person')
450+
list_filter = ('start', 'end', 'organization__locations__country')
451+
search_fields = (
452+
'pk',
453+
'title',
454+
'department',
455+
'organization__ror_display__value',
456+
'organization__custom_label__value',
457+
'organization__labels__value',
458+
'organization__aliases__value',
459+
'organization__acronyms__value',
460+
'account__first_name',
461+
'account__last_name',
462+
'account__email',
463+
'frozen_author__first_name',
464+
'frozen_author__last_name',
465+
'frozen_author__frozen_email',
466+
'preprint_author__account__first_name',
467+
'preprint_author__account__last_name',
468+
'preprint_author__account__email',
469+
)
470+
raw_id_fields = ('account', 'frozen_author',
471+
'preprint_author', 'organization')
472+
473+
def _person(self, obj):
474+
if obj:
475+
return obj.account or obj.frozen_author or obj.preprint_author
476+
else:
477+
return ''
478+
479+
401480
admin_list = [
402481
(models.AccountRole, AccountRoleAdmin),
403482
(models.Account, AccountAdmin),
@@ -427,6 +506,10 @@ class AccessRequestAdmin(admin.ModelAdmin):
427506
(models.Contacts, ContactsAdmin),
428507
(models.Contact, ContactAdmin),
429508
(models.AccessRequest, AccessRequestAdmin),
509+
(models.Organization, OrganizationAdmin),
510+
(models.OrganizationName, OrganizationNameAdmin),
511+
(models.Location, LocationAdmin),
512+
(models.ControlledAffiliation, ControlledAffiliationAdmin),
430513
]
431514

432515
[admin.site.register(*t) for t in admin_list]

src/core/forms/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from core.forms.forms import (
22
AccessRequestForm,
3+
AccountAffiliationForm,
34
AccountRoleForm,
45
AdminUserForm,
56
ArticleMetaImageForm,
67
CBVFacetForm,
78
ConfirmableForm,
89
ConfirmableIfErrorsForm,
10+
ConfirmDeleteForm,
911
EditAccountForm,
1012
EditKey,
1113
EditorialGroupForm,
@@ -24,6 +26,7 @@
2426
JournalSubmissionForm,
2527
LoginForm,
2628
NotificationForm,
29+
OrganizationNameForm,
2730
PasswordResetForm,
2831
PressJournalAttrForm,
2932
QuickUserForm,

src/core/forms/forms.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class RegistrationForm(forms.ModelForm, CaptchaForm):
183183
class Meta:
184184
model = models.Account
185185
fields = ('email', 'salutation', 'first_name', 'middle_name',
186-
'last_name', 'department', 'institution', 'country', 'orcid',)
186+
'last_name', 'orcid',)
187187
widgets = {'orcid': forms.HiddenInput() }
188188

189189
def __init__(self, *args, **kwargs):
@@ -532,7 +532,7 @@ def __init__(self, *args, **kwargs):
532532
class QuickUserForm(forms.ModelForm):
533533
class Meta:
534534
model = models.Account
535-
fields = ('email', 'salutation', 'first_name', 'last_name', 'institution',)
535+
fields = ('email', 'salutation', 'first_name', 'last_name',)
536536

537537

538538
class LoginForm(CaptchaForm):
@@ -945,3 +945,58 @@ class AccountRoleForm(forms.ModelForm):
945945
class Meta:
946946
model = models.AccountRole
947947
fields = '__all__'
948+
949+
950+
class OrganizationNameForm(forms.ModelForm):
951+
952+
class Meta:
953+
model = models.OrganizationName
954+
fields = ('value',)
955+
956+
957+
class AccountAffiliationForm(forms.ModelForm):
958+
"""
959+
A form for account holders to edit their own affiliations.
960+
Not intended for editing someone else's affiliations.
961+
"""
962+
963+
class Meta:
964+
model = models.ControlledAffiliation
965+
fields = ('title', 'department', 'is_primary', 'start', 'end')
966+
widgets = {
967+
'start': HTMLDateInput,
968+
'end': HTMLDateInput,
969+
}
970+
971+
def __init__(self, *args, **kwargs):
972+
self.account = kwargs.pop('account', None)
973+
self.organization = kwargs.pop('organization', None)
974+
super().__init__(*args, **kwargs)
975+
976+
def clean(self):
977+
cleaned_data = super().clean()
978+
query = Q(account=self.account, organization=self.organization)
979+
for key, value in cleaned_data.items():
980+
query &= Q((key, value))
981+
if self._meta.model.objects.filter(query).exists():
982+
self.add_error(
983+
None,
984+
"An affiliation with matching details already exists."
985+
)
986+
return cleaned_data
987+
988+
def save(self, commit=True):
989+
affiliation = super().save(commit=False)
990+
affiliation.account = self.account
991+
affiliation.organization = self.organization
992+
if commit:
993+
affiliation.save()
994+
return affiliation
995+
996+
997+
class ConfirmDeleteForm(forms.Form):
998+
"""
999+
A generic form for use on confirm-delete pages
1000+
where a valid form with POST data means yes, delete.
1001+
"""
1002+
pass

src/core/include_urls.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@
129129
re_path(r'^manager/user/(?P<user_id>\d+)/edit/$', core_views.user_edit, name='core_user_edit'),
130130
re_path(r'^manager/user/(?P<user_id>\d+)/history/$', core_views.user_history, name='core_user_history'),
131131

132+
## Affiliations
133+
re_path(
134+
r'^profile/organization/search/$',
135+
core_views.OrganizationListView.as_view(),
136+
name='core_organization_search'
137+
),
138+
re_path(
139+
r'^profile/organization_name/create/$',
140+
core_views.organization_name_create,
141+
name='core_organization_name_create'
142+
),
143+
re_path(
144+
r'^profile/organization_name/(?P<organization_name_id>\d+)/update/$',
145+
core_views.organization_name_update,
146+
name='core_organization_name_update'
147+
),
148+
re_path(
149+
r'^profile/organization/(?P<organization_id>\d+)/affiliation/create/$',
150+
core_views.affiliation_create,
151+
name='core_affiliation_create'
152+
),
153+
re_path(
154+
r'^profile/affiliation/(?P<affiliation_id>\d+)/update/$',
155+
core_views.affiliation_update,
156+
name='core_affiliation_update'
157+
),
158+
re_path(
159+
r'^profile/affiliation/(?P<affiliation_id>\d+)/delete/$',
160+
core_views.affiliation_delete,
161+
name='core_affiliation_delete'
162+
),
163+
132164
# Templates
133165
re_path(r'^manager/templates/$', core_views.email_templates, name='core_email_templates'),
134166

0 commit comments

Comments
 (0)