from dal import autocomplete
from django import forms
from django.conf import settings
from django.conf.urls import url
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from tabular_export.admin import export_to_csv_response
from viapy.widgets import ViafWidget
from mep.accounts.admin import AddressInline
from mep.common.admin import (CollapsedTabularInline, CollapsibleTabularInline,
NamedNotableAdmin)
from mep.footnotes.admin import FootnoteInline
from .models import (Country, InfoURL, Location, Person, Profession,
Relationship, RelationshipType)
[docs]class InfoURLInline(CollapsibleTabularInline):
model = InfoURL
fields = ('url', 'notes')
[docs]class CountryAdmin(admin.ModelAdmin):
form = CountryAdminForm
list_display = ('name', 'geonames_id', 'code')
search_fields = ('name', 'geonames_id', 'code')
fields = ['geonames_id', 'name', 'code']
class Media:
js = ['admin/geonames-lookup.js']
[docs]class RelationshipInline(CollapsedTabularInline):
'''Inline class for Relationships'''
model = Relationship
fk_name = 'from_person'
form = RelationshipInlineForm
verbose_name_plural = 'Relationships'
extra = 1
[docs]class PersonTypeListFilter(admin.SimpleListFilter):
'''Filter that for :class:`~mep.people.models.Person` that can distinguish
between people who are creators of books vs. those who are library members.
'''
# human-readable title for filter
title = 'Person Type'
# this gets used in the URL as a query param
parameter_name = 'person_type'
[docs] def lookups(self, request, model_admin):
# option tuples: left is query param name and right is human-readable name
return (
('creator', 'Creator'),
('member', 'Library Member'),
('uncategorized', 'Uncategorized')
)
[docs] def queryset(self, request, queryset):
# filter the queryset based on the selected option
if self.value() == 'creator': # is creator
return queryset.exclude(creator=None)
if self.value() == 'member': # has account
return queryset.exclude(account=None)
if self.value() == 'uncategorized': # no account or creator
return queryset.filter(account=None).filter(creator=None)
[docs]class PersonAddressInline(AddressInline):
# extend address inline for person to specify foreign key field
# and remove account from editable fields
fields = ('location', 'partial_start_date', 'partial_end_date',
'care_of_person', 'notes')
fk_name = 'person'
[docs]class PersonAdmin(admin.ModelAdmin):
'''ModelAdmin for :class:`~mep.people.models.Person`.
Uses custom template to display account subscription events and
any relationships _to_ this person (only relationships to _other_
people are edited here).
'''
form = PersonAdminForm
list_display = (
'name', 'title', 'sort_name', 'list_nationalities',
'birth_year', 'death_year', 'gender', 'profession', 'viaf_id',
'mep_id', 'account_id', 'address_count', 'in_logbooks', 'has_card',
'verified', 'updated_at', 'note_snippet')
fields = (
'title',
('name', 'sort_name'),
('slug', 'mep_id'),
('has_account', 'in_logbooks', 'has_card', 'is_creator'),
'viaf_id',
('birth_year', 'death_year'),
'gender', 'profession', 'nationalities', 'is_organization', 'verified',
'notes', 'public_notes', 'past_slugs_list')
readonly_fields = ('mep_id', 'in_logbooks', 'has_account', 'has_card',
'is_creator', 'past_slugs_list')
search_fields = ('mep_id', 'name', 'sort_name', 'notes', 'public_notes',
'viaf_id', 'slug')
list_filter = (PersonTypeListFilter, 'gender', 'profession', 'nationalities',
'is_organization')
# Note: moving relationships to last for adjacency to list of relationships
# *to* this person included in the template
inlines = [InfoURLInline, PersonAddressInline, FootnoteInline, RelationshipInline]
# by default, set sort name from name for those cases where
# only one name is known and they are the same
prepopulated_fields = {
"sort_name": ("name",),
"slug": ("sort_name",)
}
# NOTE: using a locally customized version of django's prepopulate.js
# to allow using the prepopulate behavior without slugifying the value
actions = ['merge_people', 'export_to_csv']
class Media:
js = ['admin/viaf-lookup.js']
[docs] def merge_people(self, request, queryset):
'''Consolidate duplicate person records.'''
# NOTE: using selected ids from form and ignoring queryset
# because this action is only meant for use with a few
# people at a time
# Get any querystrings including filters, pickle them as a urlencoded
# string
request.session['people_merge_filter'] = urlencode(request.GET.items())
selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
redirect = '%s?ids=%s' % (reverse('people:merge'), ','.join(selected))
return HttpResponseRedirect(redirect, status=303) # 303 = See Other
merge_people.short_description = 'Merge selected people'
merge_people.allowed_permissions = ('change', 'delete')
#: fields to be included in CSV export
export_fields = [
'id', 'name', 'sort_name', 'mep_id', 'account_id', 'birth_year',
'death_year', 'gender', 'title', 'profession', 'is_organization',
'is_creator', 'has_account', 'in_logbooks', 'has_card',
'subscription_dates', 'verified', 'updated_at', 'admin_url'
]
[docs] def csv_filename(self):
'''Generate filename for CSV download'''
return 'mep-people-%s.csv' % now().strftime('%Y%m%dT%H:%M:%S')
[docs] def tabulate_queryset(self, queryset):
'''Generator for data in tabular form, including custom fields'''
prefetched = queryset.\
prefetch_related('account_set', 'creator_set',
'account_set__event_set', 'account_set__event_set__subscription').\
select_related('profession')
for person in prefetched:
# retrieve values for configured export fields; if the attribute
# is a callable (i.e., a custom property method), call it
yield [value() if callable(value) else value
for value in (getattr(person, field) for field in self.export_fields)]
[docs] def export_to_csv(self, request, queryset=None):
'''Stream tabular data as a CSV file'''
queryset = self.get_queryset(request) if queryset is None else queryset
# use verbose names to label the columns (adapted from django-tabular-export)
# get verbose names for model fields
verbose_names = {i.name: i.verbose_name for i in queryset.model._meta.fields}
# get verbose field name if there is one; look for verbose name
# on a non-field attribute (e.g. a method); otherwise, title case the field name
headers = [verbose_names.get(field, None) or
getattr(getattr(queryset.model, field), 'verbose_name',
field.replace('_', ' ').title())
for field in self.export_fields]
return export_to_csv_response(self.csv_filename(), headers,
self.tabulate_queryset(queryset))
export_to_csv.short_description = 'Export selected people to CSV'
[docs] def get_urls(self):
'''Return admin urls; adds a custom URL for exporting all people
as CSV'''
urls = [
url(r'^csv/$', self.admin_site.admin_view(self.export_to_csv),
name='people_person_csv')
]
return urls + super(PersonAdmin, self).get_urls()
[docs] def past_slugs_list(self, instance=None):
'''list of previous slugs for this person, for read-only display'''
if instance:
return ', '.join([p.slug for p in instance.past_slugs.all()])
past_slugs_list.short_description = "Past slugs"
past_slugs_list.long_description = 'Alternate slugs from edits or merges'
[docs]class LocationAdmin(admin.ModelAdmin):
form = LocationAdminForm
list_display = ('__str__', 'name', 'street_address', 'city',
'country', 'has_notes')
# Use fieldset in order to add more instructions for looking up
# the geographic coordinates
fieldsets = (
(None, {
'fields': ('name', 'street_address', 'city', 'postal_code',
'country', 'notes')
}),
('Geographic Coordinates', {
'fields': ('latitude', 'longitude', 'mapbox_token'),
'description':
mark_safe('Use <a href="http://www.latlong.net/" target="_blank">http://www.latlong.net/</a>' +
' to find coordinates for an address. Confirm using this map, ' +
'which will update whenever the coordinates are modified.'),
}),
)
list_filter = ('country',)
search_fields = ('name', 'street_address', 'city', 'notes')
inlines = [AddressInline, FootnoteInline]
class Media:
css = {
'all': ['https://unpkg.com/leaflet@1.0.2/dist/leaflet.css',
'admin/geonames.css']
}
js = ['admin/geonames-lookup.js',
'https://unpkg.com/leaflet@1.0.2/dist/leaflet.js']
# enable default admin to see imported data
admin.site.register(Person, PersonAdmin)
admin.site.register(Country, CountryAdmin)
admin.site.register(Location, LocationAdmin)
admin.site.register(Profession, NamedNotableAdmin)
admin.site.register(RelationshipType, NamedNotableAdmin)