diff --git a/django_api/admin.py b/django_api/admin.py index 2c40527f..96cc95cb 100644 --- a/django_api/admin.py +++ b/django_api/admin.py @@ -1,500 +1,506 @@ """ Django admin module for the django_api application All admin pages inherit from import_export.admin.ImportExportModelAdmin for JSON import/export. """ from django.contrib import admin from django import forms from datetime import date, datetime import re from import_export.admin import ImportExportModelAdmin from django.contrib.admin import TabularInline from django.contrib.admin import SimpleListFilter from django.contrib.admin import RelatedOnlyFieldListFilter from django.forms.models import BaseInlineFormSet from django.utils.translation import gettext_lazy as _ #from inline_actions.admin import InlineActionsMixin #from inline_actions.admin import InlineActionsModelAdminMixin from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.utils.html import escape, mark_safe, format_html from .models import Country from .models import Language from .models import Issn from .models import Oa from .models import Publisher from .models import Journal from .models import Organization from .models import Version from .models import Licence from .models import Cost_factor_type from .models import Cost_factor from .models import Term from .models import ConditionType from .models import ConditionSubType from .models import ConditionSet from .models import OrganizationCondition from .models import JournalCondition # Register your models here. @admin.register(Issn) class IssnAdmin(ImportExportModelAdmin): # TODO use RelatedOnlyFieldListFilter for publisher when data allows it list_filter = ('issn_type', 'journal__publisher__name', ) list_display = ("id", "issn", 'journal') class IssnInline(admin.TabularInline): model = Issn readonly_fields = ('issn', 'issn_type',) # This Inline is stricty read-only for the moment def has_change_permission(self, request, obj=None): return False def has_add_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False @admin.register(Journal) class JournalAdmin(ImportExportModelAdmin): list_display = ("id", "name", "get_journal_issns",) # TODO use RelatedOnlyFieldListFilter for publisher when data allows it list_filter = ('oa_status', 'publisher', ) filter_horizontal = ('publisher', 'language', ) search_fields = ('name', 'classIssn__issn') inlines = (IssnInline, ) @admin.display(description='ISSNs') def get_journal_issns(self, obj): return list(Issn.objects.filter(journal=obj)) @admin.register(Language) class LanguageAdmin(ImportExportModelAdmin): pass @admin.register(Organization) class OrganizationAdmin(ImportExportModelAdmin): list_display = ("id", "name") list_filter = ('is_funder', ('country', RelatedOnlyFieldListFilter)) filter_horizontal = ('country', ) search_fields = ('name', ) @admin.register(Version) class VersionAdmin(ImportExportModelAdmin): pass @admin.register(Country) class CountryAdmin(ImportExportModelAdmin): pass @admin.register(Oa) class OaAdmin(ImportExportModelAdmin): pass @admin.register(Publisher) class PublisherAdmin(ImportExportModelAdmin): list_display = ("id", "name") # Experimental: what will happen with 200+ countries in the database? list_filter = (('country', RelatedOnlyFieldListFilter), ) filter_horizontal = ('country', ) @admin.register(Term) class TermAdmin(ImportExportModelAdmin): list_display = ("id", "__str__", ) list_filter = ('version', 'licence', 'ir_archiving') search_fields = ("id", "comment", "embargo_months") filter_horizontal = ('version', 'cost_factor', 'licence', ) # textarea input is better for comments def get_form(self, request, obj=None, **kwargs): kwargs['widgets'] = {'comment': forms.Textarea} return super().get_form(request, obj, **kwargs) @admin.register(ConditionType) class ConditionTypeAdmin(ImportExportModelAdmin): list_display = ("id", "condition_issuer") @admin.register(ConditionSubType) class ConditionTypeAdmin(ImportExportModelAdmin): list_display = ("id", "label") class JournalConditionFormset(forms.BaseInlineFormSet): def __init__(self, *args, **kwargs): super(JournalConditionFormset, self).__init__(*args, **kwargs) self.queryset = self.queryset.select_related("journal", 'condition_set') #class JournalConditionInline(InlineActionsMixin, TabularInline): class JournalConditionInline(TabularInline): model = JournalCondition fields = ('journal', 'valid_from', 'valid_until') extra = 1 #inline_actions = ['connect_all_journals'] autocomplete_fields = ('journal', ) formset = JournalConditionFormset """ def connect_all_journals(self, request, obj, parent_obj=None): # Do stuff here, then return None to go to current view return None connect_all_journals.short_description = ("Connect Condition Set with some or all Journals") """ def get_queryset(self, request): qs = super(JournalConditionInline, self).get_queryset(request).prefetch_related() return qs.select_related('journal') class OrganizationConditionFormset(forms.BaseInlineFormSet): def __init__(self, *args, **kwargs): super(OrganizationConditionFormset, self).__init__(*args, **kwargs) self.queryset = self.queryset.select_related("organization", 'condition_set') class OrganizationConditionInline(TabularInline): #class OrganizationConditionInline(InlineActionsMixin, TabularInline): # model = OrganizationCondition model = ConditionSet.organization.through extra = 1 autocomplete_fields = ('organization', ) formset = OrganizationConditionFormset @admin.action(description='Apply selected condition sets to multiple Journals') def connect_with_all_journals(modeladmin, request, queryset): """ Action applicable to one or more ConditionSets: connect with a list of Journals by entering the ISSN for the relevant ones, or all Journals if no ISSN is given. Start and end dates must be provided during the action. This action is useful to connect a new organization policy or publishing agreement with the journals to which it applies. """ if request.POST.get('apply'): try: valid_from = date.fromisoformat(request.POST['valid_from']) valid_until = date.fromisoformat(request.POST['valid_until']) issn_list = set([x for x in re.split(' |,|;|\n|\r|\t', request.POST['issn_list']) if len(x) > 0]) print(issn_list) if valid_from > valid_until: raise ValueError # print((valid_from, valid_until)) if len(issn_list) == 0: all_journals = Journal.objects.all() else: journal_ids = list(Issn.objects.filter(issn__in=issn_list).values_list('journal', flat=True).distinct()) # print(journal_ids) all_journals = Journal.objects.filter(id__in=journal_ids) print(all_journals) print(len(issn_list), len(all_journals)) # all_journals =[] # The following block could certainly be optimized! AB 2021-08-12 for condition_set in queryset: # print('-----------------') # print(condition_set) for j in all_journals: # print(j) # search for existing connections existing_connections = JournalCondition.objects.filter(journal=j, condition_set=condition_set, valid_from__lt=date.today(), valid_until__gt=date.today()) # print(existing_connections) if len(existing_connections) == 0: new_journal_condition = JournalCondition(journal=j, condition_set=condition_set, valid_from=valid_from, valid_until=valid_until) new_journal_condition.save() else: # This should not happen, or could it? print(f'{j} already connected with {condition_set}') return None except ValueError: pass return render(request, 'admin/get_validity_dates.html', context={'queryset': queryset, 'objects': 'journals'}) @admin.action(description='Apply selected condition sets to all Organizations') def connect_with_all_organizations(modeladmin, request, queryset): """ Action applicable to one or more ConditionSets: connect with all Organizations. Start and end dates must be provided during the action. This action is useful to connect a new journal policy with all known organizations. """ if request.POST.get('apply'): try: valid_from = date.fromisoformat(request.POST['valid_from']) valid_until = date.fromisoformat(request.POST['valid_until']) if valid_from > valid_until: raise ValueError # print((valid_from, valid_until)) all_orgs = Organization.objects.all() for condition_set in queryset: # print('-----------------') # print(condition_set) for o in all_orgs: # print(o) # search for existing connections existing_connections = OrganizationCondition.objects.filter(organization=o, condition_set=condition_set, valid_from__lt=date.today(), valid_until__gt=date.today()) # print(existing_connections) if len(existing_connections) == 0: new_organization_condition = OrganizationCondition(organization=o, condition_set=condition_set, valid_from=valid_from, valid_until=valid_until) new_organization_condition.save() return None except ValueError: pass return render(request, 'admin/get_validity_dates.html', context={'queryset': queryset, 'objects': 'organizations'}) @admin.action(description='Set valid_until date') def end_validity(modeladmin, request, queryset): """ Action to set the end date for selected Journal-Condition relationships. This action was introduced to add validity dates to batch-uploaded JournalConditions that lacked this information. """ if request.POST.get('apply'): try: valid_until = date.fromisoformat(request.POST['date']) queryset.update(valid_until=valid_until) return None except ValueError: pass return render(request, 'admin/get_single_validity_date.html', context={'queryset': queryset.prefetch_related(), 'limit': 'end', 'objects': 'selected journal-condition connections'}) @admin.action(description='Set valid_from date') def start_validity(modeladmin, request, queryset): """ Action to set the start date for selected Journal-Condition relationships. This action was introduced to add validity dates to batch-uploaded JournalConditions that lacked this information. """ if request.POST.get('apply'): try: valid_from = date.fromisoformat(request.POST['date']) queryset.update(valid_from=valid_from) return None except ValueError: pass return render(request, 'admin/get_single_validity_date.html', context={'queryset': queryset, 'limit': 'start', 'objects': 'selected journal-condition connections'}) class ConditionSetAdminForm(forms.ModelForm): class Meta: model = ConditionSet fields = ['condition_type', 'subtype', 'term', 'source', 'comment', ] def __init__(self, *args, **kwargs): #start = datetime.now() super(ConditionSetAdminForm, self).__init__(*args, **kwargs) self.fields['term'].queryset = Term.objects.all().prefetch_related('licence', 'cost_factor', 'version') #print('ConditionSetAdminForm.__init__(): ', datetime.now(), datetime.now()-start) @admin.register(ConditionSet) class ConditionSetAdmin(ImportExportModelAdmin): # class ConditionSetAdmin(InlineActionsModelAdminMixin, ImportExportModelAdmin): list_display = ("id", "condition_type", "comment") search_fields = ['organization__name', 'journal__name', 'comment', 'id', 'condition_type__condition_issuer'] - list_filter = ('condition_type', ) + list_filter = ('condition_type', 'journal__publisher__name', 'organization__name', ) form = ConditionSetAdminForm filter_horizontal = ('term', ) exclude = ('organization', 'journal', ) inlines = (OrganizationConditionInline, JournalConditionInline, ) # inlines = (OrganizationConditionInline, ) actions = [connect_with_all_journals, connect_with_all_organizations] # textarea input is better for comments def get_form(self, request, obj=None, **kwargs): kwargs['widgets'] = {'comment': forms.Textarea} return super().get_form(request, obj, **kwargs) def get_queryset(self, request): #start = datetime.now() test_model_qs = super(ConditionSetAdmin, self).get_queryset(request) test_model_qs = test_model_qs.prefetch_related('organization', 'journal') #print('ConditionSetAdmin.get_queryset(): ', datetime.now(), datetime.now()-start) return test_model_qs class XConditionValidListFilter(SimpleListFilter): """ Human-readable title which will be displayed in the right admin sidebar just above the filter options. """ title = _('currently valid') # Parameter for the filter that will be used in the URL query. parameter_name = 'valid' def lookups(self, request, model_admin): """ Returns a list of tuples. The first element in each tuple is the coded value for the option that will appear in the URL query. The second element is the human-readable name for the option that will appear in the right sidebar. """ return ( ('true', _('True')), ('false', _('False')), ) def queryset(self, request, queryset): """ Returns the filtered queryset based on the value provided in the query string and retrievable via `self.value()`. """ # Compare the requested value (either '80s' or '90s') # to decide how to filter the queryset. if self.value() == 'true': return queryset.filter(valid_from__lte=date.today(), valid_until__gte=date.today()) if self.value() == 'false': return queryset.exclude(valid_from__lte=date.today(), valid_until__gte=date.today()) class ConditionSetListDynamicFilter(SimpleListFilter): + """ Dynamic filter-by-publisher for ConditionSets. Only Publishers of Journals + connected to the currently displayed ConditionSets are proposed. + """ title = _('condition sets (publisher-dependant)') parameter_name = 'condition_set' def lookups(self, request, model_admin): if 'journal__publisher__name' in request.GET: # A publisher name filter is in effect journal_publisher_name = request.GET['journal__publisher__name'] print([journal_publisher_name]) #cs_by_publisher = model_admin.model.objects.filter(journal__publisher__name=journal_publisher_name) #print(cs_by_publisher) #jcs_by_publisher = model_admin.model.objects.all().filter(journal__publisher__name=journal_publisher_name).prefetch_related() #condition_sets = set([c.condition_set for c in model_admin.model.objects.all().filter(journal__publisher__name=journal_publisher_name)]) #condition_sets = sorted(list(condition_sets)) cs = model_admin.model.objects.filter(journal__publisher__name=journal_publisher_name).values_list('condition_set') condition_sets = ConditionSet.objects.filter(id__in=cs).order_by('id') print(condition_sets) #condition_sets = ConditionSet.objects.filter(journal__publisher__name=journal_publisher_name).order_by('id') #condition_sets = model_admin.model.objects.filter(journal__publisher__name=journal_publisher_name). else: #condition_sets = set([c.condition_set for c in model_admin.model.objects.all()]) condition_sets = ConditionSet.objects.all().order_by('id') return [(s.id, str(s)) for s in condition_sets] def queryset(self, request, queryset): if self.value(): return queryset.filter(condition_set__id__exact=self.value()) @admin.register(OrganizationCondition) class OrganizationConditionAdmin(ImportExportModelAdmin): - + """ Organization-ConditionSet connection admin page + """ @admin.display(description='Condition Set') def link_to_conditionset(self, obj): + """ Calculated field for the list display: link to the relevant ConditionSet + """ link = reverse("admin:django_api_conditionset_change", args=[obj.condition_set.id]) return format_html(f'{obj.condition_set}') search_fields = ("id", "organization__name", "condition_set__id") list_display = ("id", "organization_name", "link_to_conditionset", "valid_from", "valid_until") list_select_related = ('condition_set', ) list_filter = ('condition_set__condition_type', XConditionValidListFilter, # This has become very slow on 2021-12-09, will revisit later #('condition_set', RelatedOnlyFieldListFilter), 'condition_set__id') def organization_name(self, obj): return obj.organization.name def get_queryset(self, request): qs = super(OrganizationConditionAdmin, self).get_queryset(request).prefetch_related() return qs.select_related('condition_set', 'organization', 'condition_set__condition_type', ) @admin.register(Licence) class LicenceAdmin(ImportExportModelAdmin): pass @admin.register(JournalCondition) class JournalConditionAdmin(ImportExportModelAdmin): @admin.display(description='Condition Set') def link_to_conditionset(self, obj): link = reverse("admin:django_api_conditionset_change", args=[obj.condition_set.id]) return format_html(f'{obj.condition_set}') search_fields = ("id", "journal__name", "condition_set__id") list_display = ("id", "journal_name", "link_to_conditionset", "valid_from", "valid_until") list_filter = ('condition_set__condition_type', XConditionValidListFilter, 'journal__publisher__name', ConditionSetListDynamicFilter) #list_filter = ('condition_set__condition_type', XConditionValidListFilter, # 'journal__publisher__name',) actions = (end_validity, start_validity, ) def journal_name(self, obj): return obj.journal.name def get_queryset(self, request): qs = super(JournalConditionAdmin, self).get_queryset(request) return qs.select_related('condition_set', 'journal') # unsuccessful attempt # def formfield_for_foreignkey(self, db_field, request, **kwargs): # if db_field.name == "journal": # kwargs["queryset"] = Journal.objects.filter(publisher__name__in=Publisher.objects.order_by().values('name').distinct()) # return super().formfield_for_foreignkey(db_field, request, **kwargs) @admin.register(Cost_factor) class Cost_factorAdmin(ImportExportModelAdmin): list_display = ("id", "comment", "amount", "symbol") list_filter = ('cost_factor_type', 'symbol') # textarea input is better for comments def get_form(self, request, obj=None, **kwargs): kwargs['widgets'] = {'comment': forms.Textarea} return super().get_form(request, obj, **kwargs) @admin.register(Cost_factor_type) class Cost_factor_typeAdmin(ImportExportModelAdmin): list_display = ("id", "name")