from collections import OrderedDict, defaultdict
from dal import autocomplete
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.humanize.templatetags.humanize import ordinal
from django.core.exceptions import MultipleObjectsReturned
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import Http404, HttpResponsePermanentRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.html import format_html, strip_tags
from django.utils.safestring import mark_safe
from django.views.generic import DetailView, ListView
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormMixin, FormView
from djiffy.models import Canvas
from mep.accounts.models import Address, Event
from mep.accounts.templatetags.account_tags import as_ranges
from mep.common import SCHEMA_ORG
from mep.common.utils import absolutize_url, alpha_pagelabels
from mep.common.views import (AjaxTemplateMixin, FacetJSONMixin,
LabeledPagesMixin, SolrLastModifiedMixin,
LoginRequiredOr404Mixin, RdfViewMixin)
from mep.people.forms import MemberSearchForm, PersonMergeForm
from mep.people.geonames import GeoNamesAPI
from mep.people.models import Country, Location, Person
from mep.people.queryset import PersonSolrQuerySet
[docs]class MembersList(LabeledPagesMixin, SolrLastModifiedMixin, ListView,
FormMixin, AjaxTemplateMixin, FacetJSONMixin, RdfViewMixin):
'''List page for searching and browsing library members.'''
model = Person
page_title = "Members"
page_description = "Search and browse members by name and filter " + \
"by membership dates, birth date, and demographics."
template_name = 'people/member_list.html'
ajax_template_name = 'people/snippets/member_results.html'
paginate_by = 100
context_object_name = 'members'
rdf_type = SCHEMA_ORG.SearchResultsPage
solr_lastmodified_filters = {'item_type': 'person'}
form_class = MemberSearchForm
# cached form instance for current request
_form = None
#: initial form values
initial = {
'sort': 'name'
}
#: mappings for Solr field names to form aliases
range_field_map = {
'account_years': 'membership_dates',
}
#: fields to generate stats on in self.get_ranges
stats_fields = ('account_years', 'birth_year')
[docs] def get_range_stats(self):
"""Return the min and max for fields specified in
:class:`MembershipList`'s stats_fields
:returns: Dictionary keyed on form field name with a tuple of
(min, max) as integers. If stats are not returned from the field,
the key is not added to a dictionary.
:rtype: dict
"""
stats = PersonSolrQuerySet().stats(*self.stats_fields).get_stats()
min_max_ranges = {}
if not stats:
return min_max_ranges
for name in self.stats_fields:
try:
min_year = int(stats['stats_fields'][name]['min'])
max_year = int(stats['stats_fields'][name]['max'])
# map to form field name if an alias is provided
min_max_ranges[self.range_field_map.get(name, name)] \
= (min_year, max_year)
# If the field stats are missing, min and max will be NULL,
# rendered as None.
# The TypeError will catch and pass returning an empty entry
# for that field but allowing others to be passed on.
except TypeError:
pass
return min_max_ranges
#: name query alias field syntax (type defaults to edismax in solr config)
search_name_query = '{!qf=$name_qf pf=$name_pf v=$name_query}'
# map form sort to solr sort field
solr_sort = {
'relevance': '-score',
'name': 'sort_name_isort'
}
[docs] def get_queryset(self):
sqs = PersonSolrQuerySet() \
.facet_field('has_card') \
.facet_field('gender', missing=True, exclude='gender') \
.facet_field('nationality', exclude='nationality', sort='value',
missing=True) \
.facet_field('arrondissement', exclude='arrondissement',
sort='value')
form = self.get_form()
# empty queryset if not valid
if not form.is_valid():
sqs = sqs.none()
# when form is valid, check for search term and filter queryset
else:
search_opts = form.cleaned_data
if search_opts['query']:
sqs = sqs.search(self.search_name_query) \
.raw_query_parameters(name_query=search_opts['query']) \
.also('score') # include relevance score in results
if search_opts['has_card']:
sqs = sqs.filter(has_card=search_opts['has_card'])
if search_opts['gender']:
sqs = sqs.filter(gender__in=search_opts['gender'], tag='gender')
if search_opts['nationality']:
# wrap filter value in quotes if there are spaces
nationality_list = ['"%s"' % val if ' ' in val else val
for val in search_opts['nationality']]
sqs = sqs.filter(nationality__in=nationality_list,
tag='nationality')
if search_opts['arrondissement']:
# strip off ordinal letters and filter on numeric arrondissement
sqs = sqs.filter(arrondissement__in=[
'%s' % val[:-2] for val in search_opts['arrondissement']
], tag='arrondissement')
# range filter by membership dates, if set
if search_opts['membership_dates']:
sqs = sqs.filter(
account_years__range=search_opts['membership_dates'])
# range filter by birth year, if set
if search_opts['birth_year']:
sqs = sqs.filter(birth_year__range=search_opts['birth_year'])
# order based on solr name for search option
sqs = sqs.order_by(self.solr_sort[search_opts['sort']])
self.queryset = sqs
return sqs
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
facets = self.object_list.get_facets().get('facet_fields', None)
error_message = ''
# facets are not set if there is an error on the query
if facets:
# convert arrondissement numbers into ordinals for display
facets['arrondissement'] = OrderedDict([
(ordinal(val), count)
for val, count in facets['arrondissement'].items()
])
self._form.set_choices_from_facets(facets)
else:
# if facets are not set, the query errored
error_message = 'Something went wrong.'
context.update({
'page_title': self.page_title,
'page_description': self.page_description,
'error_message': error_message
})
return context
[docs] def get_page_labels(self, paginator):
'''generate labels for pagination'''
# if form is invalid, page labels should show 'N/A'
form = self.get_form()
if not form.is_valid():
return [(1, 'N/A')]
# when sorting by relevance, use default page label logic
if form.cleaned_data['sort'] == 'relevance':
return super().get_page_labels(paginator)
# otherwise, when sorting by alpha, generate alpha page labels
# Only return sort name; get everything at once to avoid
# hitting Solr for each page / item.
pagination_qs = self.queryset.only('sort_name') \
.get_results(rows=100000)
alpha_labels = alpha_pagelabels(paginator, pagination_qs,
lambda x: x['sort_name'][0],
max_chars=4)
# alpha labels is a dict; use items to return list of tuples
return alpha_labels.items()
[docs] def get_absolute_url(self):
'''Get the full URI of this page.'''
return absolutize_url(reverse('people:members-list'))
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(self.page_title, self.get_absolute_url()),
]
[docs]class MemberPastSlugMixin:
'''View mixin to handle redirects for previously used slugs.
If the main view logic raises a 404, looks for a library member
by past slug; if one is found, redirects to the corresponding
member detail page with the new slug.
'''
def get(self, request, *args, **kwargs):
try:
return super().get(request, *args, **kwargs)
except Http404:
# if not found, check for a match on a past slug
person = Person.objects.library_members() \
.filter(past_slugs__slug=self.kwargs['slug']).first()
# if found, redirect to the correct url for this view
if person:
# patch in the correct slug for use with get absolute url
self.kwargs['slug'] = person.slug
self.object = person # used by member detail absolute url
return HttpResponsePermanentRedirect(self.get_absolute_url())
# otherwise, raise the 404
raise
[docs]class MemberLastModifiedListMixin(SolrLastModifiedMixin):
'''last modified mixin with common logic for all single-member views'''
[docs] def get_solr_lastmodified_filters(self):
# NOTE: slug_s because not using aliased queryset
return {'item_type': 'person', 'slug_s': self.kwargs['slug']}
[docs]class MemberDetail(MemberPastSlugMixin, MemberLastModifiedListMixin,
DetailView, RdfViewMixin):
'''Detail page for a single library member.'''
model = Person
template_name = 'people/member_detail.html'
context_object_name = 'member'
rdf_type = SCHEMA_ORG.ProfilePage
# format string for page description
page_description = 'Shakespeare and Company lending library member %s'
[docs] def get_queryset(self):
# throw a 404 if a non-member is accessed via this route
return super().get_queryset().library_members()
[docs] def get_absolute_url(self):
'''Get the full URI of this page.'''
return absolutize_url(self.object.get_absolute_url())
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# add account to context for convenience
account = self.object.account_set.first()
context['account'] = account
month_counts = defaultdict(int)
# count book events by month; known years only
for event in account.event_set.known_years().book_activities():
if event.start_date:
month_counts[event.start_date.strftime('%Y-%m-01')] += 1
# if end date is different from start date, count that also
if event.end_date and event.start_date != event.end_date:
month_counts[event.end_date.strftime('%Y-%m-01')] += 1
account_date_ranges = account.event_date_ranges()
account_years = account.event_years
# data for member timeline visualization
context['timeline'] = {
'membership_activities': [{
'startDate': event.start_date.isoformat()
if event.start_date else '',
'endDate': event.end_date.isoformat()
if event.end_date else '',
'type': event.event_type
} for event in account.event_set.membership_activities()
.known_years()],
'book_activities': [{
'startDate': start_date,
'count': count
} for start_date, count in month_counts.items()],
'activity_ranges': [{
'startDate': start.isoformat(),
'endDate': end.isoformat()
} for start, end in account_date_ranges]
}
# plottable locations for member address map visualization, which
# is a leaflet map that will consume JSON address data
# NOTE probably refactor this into a queryset method for use on
# members search map
addresses = Address.objects.filter(account=account) \
.filter(location__latitude__isnull=False) \
.filter(location__longitude__isnull=False)
# NOTE probably refactor this into a method on Location for feeding
# to leaflet; also use below
context['addresses'] = [
{
# these fields are taken from Location unchanged
'name': address.location.name,
'street_address': address.location.street_address,
'city': address.location.city,
'arrondissement': address.location.arrondissement_ordinal(),
# lat/long aren't JSON serializable so we need to do this
'latitude': str(address.location.latitude),
'longitude': str(address.location.longitude),
# NOTE not currently using dates as they're not entered yet
}
for address in addresses]
# address of the lending library itself; automatically available from
# migration mep/people/migrations/0014_library_location.py
try:
library = Location.objects.get(name='Shakespeare and Company')
context['library_address'] = {
'name': library.name,
'street_address': library.street_address,
'city': library.city,
'arrondissement': library.arrondissement_ordinal(),
'latitude': str(library.latitude),
'longitude': str(library.longitude),
}
except Location.DoesNotExist:
# if we can't find library's address send 'null' & don't render it
context['library_address'] = None
# text-only readable version of membership years for meta description
membership_years = strip_tags(as_ranges(account_years)
.replace('</span>', ',')).rstrip(',')
# config settings used to render the map; set in local_settings.py
context.update({
'mapbox_token': getattr(settings, 'MAPBOX_ACCESS_TOKEN', ''),
'mapbox_basemap': getattr(settings, 'MAPBOX_BASEMAP', ''),
'paris_overlay': getattr(settings, 'PARIS_OVERLAY', ''),
'account_years': account_years,
# metadata for social preview
'page_title': self.object.firstname_last,
'page_description': self.page_description % membership_years
})
return context
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(MembersList.page_title, MembersList().get_absolute_url()),
(self.object.short_name, self.get_absolute_url())
]
[docs]class MembershipActivities(MemberPastSlugMixin, MemberLastModifiedListMixin,
ListView, RdfViewMixin):
'''Display a list of membership activities (subscriptions, renewals,
and reimbursements) for an individual member.'''
model = Event
template_name = 'people/membership_activities.html'
# tooltip text shown to explain the 'plan' column in the table
PLAN_TOOLTIP = 'What are the lending library “plans”?'
[docs] def get_queryset(self):
# filter to requested person, then get membership activities
return super().get_queryset() \
.filter(account__persons__slug=self.kwargs['slug']) \
.membership_activities()
[docs] def get_context_data(self, **kwargs):
# should 404 if not a person or valid person but not a library member
# store member before calling super so available for breadcrumbs
self.member = get_object_or_404(Person.objects.library_members(),
slug=self.kwargs['slug'])
context = super().get_context_data(**kwargs)
context.update({
'member': self.member,
'plan_tooltip': self.PLAN_TOOLTIP,
'page_title': '%s Membership Activity' % self.member.firstname_last
})
return context
[docs] def get_absolute_url(self):
'''Get the full URI of this page.'''
return absolutize_url(reverse('people:membership-activities',
kwargs=self.kwargs))
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(MembersList.page_title, MembersList().get_absolute_url()),
(self.member.short_name,
absolutize_url(self.member.get_absolute_url())),
('Membership', self.get_absolute_url())
]
[docs]class BorrowingActivities(MemberPastSlugMixin, MemberLastModifiedListMixin,
ListView, RdfViewMixin):
'''Display a list of book-related activities (borrows, purchases, gifts)
for an individual member.'''
model = Event
template_name = 'people/borrowing_activities.html'
[docs] def get_queryset(self):
# filter to requested person, then get book activities
return super().get_queryset() \
.filter(account__persons__slug=self.kwargs['slug']) \
.book_activities() \
.select_related('borrow', 'purchase', 'work') \
.prefetch_related('work__creators', 'work__creator_set',
'work__creator_set__creator_type')
[docs] def get_context_data(self, **kwargs):
# should 404 if not a person or valid person but not a library member
# store member before calling super so available for breadcrumbs
self.member = get_object_or_404(Person.objects.library_members(),
slug=self.kwargs['slug'])
context = super().get_context_data(**kwargs)
context.update({
'member': self.member,
'page_title': '%s Borrowing Activity' % self.member.firstname_last
})
return context
[docs] def get_absolute_url(self):
'''Get the full URI of this page.'''
return absolutize_url(reverse('people:borrowing-activities',
kwargs=self.kwargs))
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(MembersList.page_title, MembersList().get_absolute_url()),
(self.member.short_name,
absolutize_url(self.member.get_absolute_url())),
('Borrowing', self.get_absolute_url())
]
[docs]class MemberCardList(MemberPastSlugMixin, MemberLastModifiedListMixin,
ListView, RdfViewMixin):
'''Card thumbnails for lending card associated with a single library
member.'''
model = Canvas
template_name = 'people/member_cardlist.html'
context_object_name = 'cards'
[docs] def get_queryset(self):
# find the associated member; 404 if not found or not a library member
self.member = get_object_or_404(Person.objects.library_members(),
slug=self.kwargs['slug'])
# return all canvas objects for this member
return self.member.account_set.first().member_card_images()
[docs] def get_absolute_url(self):
'''Full URI for member card list page.'''
return absolutize_url(reverse('people:member-card-list',
kwargs=self.kwargs))
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(MembersList.page_title, MembersList().get_absolute_url()),
(self.member.short_name,
absolutize_url(self.member.get_absolute_url())),
('Cards', self.get_absolute_url())
]
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page_title = 'Lending library cards for %s' % \
self.member.firstname_last
card_count = self.object_list.count()
page_description = '%d card%s' % \
(card_count, 's' if card_count != 1 else '')
context.update({
'member': self.member,
# social preview
'page_title': page_title,
'page_description': page_description,
'page_iiif_image': getattr(self.object_list.first(), 'image', None)
})
return context
[docs]class MemberCardDetail(MemberPastSlugMixin, MemberLastModifiedListMixin,
DetailView, RdfViewMixin):
'''Card image viewer for image of a single lending card page
associated with a single library member.'''
model = Canvas
template_name = 'people/member_card_detail.html'
context_object_name = 'card'
[docs] def get_object(self):
# find the associated member; 404 if not found or not a library member
self.member = get_object_or_404(Person.objects.library_members(),
slug=self.kwargs['slug'])
# images associated with lending card bibliography OR footnote events
self.cards = self.member.account_set.first().member_card_images()
# because this is a union queryset, filter by id manually
card = None
for image in self.cards:
if image.short_id == self.kwargs['short_id']:
card = image
break
if not card:
# 404 if we didn't find the requested card
raise Http404
# use card dates for label
card_dates = card.footnote_set.event_date_range()
# used for page title and breadcrumb label;
if card_dates:
label = card_dates[0].year
if card_dates[1].year != card_dates[0].year:
label = '%s – %s' % (label, card_dates[1].year)
elif card.footnote_set.exists():
# if there are footnotes but no dates, label as unknown
label = 'Unknown'
else:
# if there are no footnotes, label as Blank
label = 'Blank'
self.label = label
return card
[docs] def get_absolute_url(self):
'''Full URI for member card list page.'''
return absolutize_url(reverse('people:member-card-detail',
kwargs=self.kwargs))
[docs] def get_breadcrumbs(self):
'''Get the list of breadcrumbs and links to display for this page.'''
return [
('Home', absolutize_url('/')),
(MembersList.page_title, MembersList().get_absolute_url()),
(self.member.short_name,
absolutize_url(self.member.get_absolute_url())),
('Cards', absolutize_url(reverse('people:member-card-list',
kwargs={'slug': self.member.slug}))),
(self.label, self.get_absolute_url())
]
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# get all cards (canvases) from the manifest and store their ids
card_ids = list(card.short_id for card in self.cards)
# create a paginator with 1 card per page and get the current "page"
paginator = Paginator(card_ids, 1)
current_index = card_ids.index(self.object.short_id)
card_page = paginator.page(current_index + 1) # 1-based page index
# add next/previous page ids to generate links, if any
if card_page.has_previous():
context['prev_card_id'] = card_ids[current_index - 1]
if card_page.has_next():
context['next_card_id'] = card_ids[current_index + 1]
# find all events associated with this card for the current member
member_events = self.object.footnote_set.events() \
.filter(account__persons=self.member)
# NOTE does using paginator get us anything here? maybe revisit
context.update({
'member': self.member,
'label': self.label,
'events': member_events,
'card_page': card_page,
'cards': self.cards,
# metadata for social preview
'page_title': '%s lending library card for %s' % \
(self.label, self.member.firstname_last),
'page_iiif_image': context['card'].image
})
return context
[docs]class MembershipGraphs(LoginRequiredOr404Mixin, TemplateView):
model = Person
template_name = 'people/member_graphs.html'
def get_context_data(self):
context = super().get_context_data()
# use facets to get member totals by month and year
sqs = PersonSolrQuerySet() \
.facet_field('account_yearmonths', sort='index', limit=1000) \
.facet_field('logbook_yearmonths', sort='index', limit=1000) \
.facet_field('card_yearmonths', sort='index', limit=1000)
facets = sqs.get_facets()['facet_fields']
context['data'] = {
# convert into a format that's easier to use with javascript/d3
'members': [{
'startDate': '%s-%s-01' % (yearmonth[:4], yearmonth[-2:]),
'count': count
} for yearmonth, count in facets['account_yearmonths'].items()
],
'logbooks': [{
'startDate': '%s-%s-01' % (yearmonth[:4], yearmonth[-2:]),
'count': count
} for yearmonth, count in facets['logbook_yearmonths'].items()
],
'cards': [{
'startDate': '%s-%s-01' % (yearmonth[:4], yearmonth[-2:]),
'count': count
} for yearmonth, count in facets['card_yearmonths'].items()
]
}
# generate version to output as tabular data
logbook_month_max = 0
# NOTE: in django templates, these defaultdicts return default value
# when attempting to iterate, retrieve items, keys, etc
# (accessing by index instead of trying method first?)
logbooks = defaultdict(lambda: [0] * 12)
for yearmonth, count in facets['logbook_yearmonths'].items():
logbook_month_max = max(logbook_month_max, count)
logbooks[int(yearmonth[:4])][int(yearmonth[-2:]) - 1] = count
cards_month_max = 0
cards = defaultdict(lambda: [0] * 12)
for yearmonth, count in facets['card_yearmonths'].items():
cards_month_max = max(cards_month_max, count)
cards[int(yearmonth[:4])][int(yearmonth[-2:]) - 1] = count
members = defaultdict(lambda: [0] * 12)
for yearmonth, count in facets['account_yearmonths'].items():
members[int(yearmonth[:4])][int(yearmonth[-2:]) - 1] = count
card_percents = {}
for year, counts in cards.items():
member_counts = members[year]
percents = []
for index, value in enumerate(counts):
if member_counts[index]:
percents.append(value / member_counts[index])
else:
percents.append('-')
card_percents[year] = percents
context['tabular_data'] = {
'years': range(1919, 1942), # workaround for iteration problem
'logbooks': logbooks,
'logbooks_month_max': logbook_month_max,
'cards': cards,
'cards_month_max': cards_month_max,
'members': members,
'card_percents': card_percents
}
return context
[docs]class GeoNamesLookup(autocomplete.Select2ListView):
'''GeoNames ajax lookup for use as autocomplete.
Optional mode parameter to restrict to countries only.
'''
mode = None
[docs] def get(self, request, mode=None, *args, **kwargs):
""""Return option list json response."""
geo_api = GeoNamesAPI()
extra_args = {}
self.mode = mode
# restrict to countries when requested
if self.mode == 'country':
extra_args.update({
'feature_class': 'A',
'feature_code': 'PCLI',
})
results = geo_api.search(self.q, max_rows=50, name_start=True,
**extra_args)
return JsonResponse({
'results': [dict(
id=geo_api.uri_from_id(item['geonameId']),
text=self.get_label(item),
name=item['name'],
country_code=item['countryCode'],
# lat & long included in data to make them available for
# javascript to populateform fields
lat=item['lat'],
lng=item['lng']
) for item in results],
})
[docs] def get_label(self, item):
'''display country for context, if available'''
if self.mode != 'country' and 'countryName' in item:
# FIXME: shouldn't ever display countryname if item is a country
return '''%(name)s, %(countryName)s''' % item
return item['name']
[docs]class PersonAutocomplete(autocomplete.Select2QuerySetView):
'''
Basic person autocomplete lookup, for use with django-autocomplete-light.
Use Q objects to help distinguish people using mepid.
'''
[docs] def get_result_label(self, person):
'''
Provide a more detailed result label for the people autocomplete that
can help disambiguate people.
'''
# Fields that will be formatted before interpolation
labels = {
'main_string': '',
'bio_dates': '',
'note_string': '',
}
# title and name, stripped in case title is absent so no stray space
labels['main_string'] = \
('%s %s' % (person.title, person.name)).strip()
# format birth-death in a familiar pattern ( - )
if person.birth_year or person.death_year:
labels['bio_dates'] = \
' (%s – %s)' % (person.birth_year, person.death_year)
# get the first few words of any notes
if person.notes:
list_notes = person.notes.split()
labels['note_string'] = ' '.join(list_notes[:5])
# padding id with a space so that it looks nice in the formatted
# html and we don't have to worry about stripping it in the
# interpolated text.
labels['mep_id'] = (' %s' % person.mep_id) if person.mep_id else ''
if not labels['bio_dates'] and not labels['note_string']:
# in situations where there are none of the above,
# pull the first event
if person.account_set.first():
event = Event.objects.filter(
account=person.account_set.first()
).order_by('start_date').first()
# if it has a first event (not all do), return that event
if event:
labels['start_date'] = event.start_date
labels['end_date'] = (event.end_date
if event.end_date else '')
labels['type'] = event.event_type
return format_html(
'<strong>{main_string}</strong>'
'{mep_id} <br />{type} '
'({start_date} – {end_date})'.strip(),
**labels
)
return format_html('<strong>{main_string}</strong>{mep_id}',
**labels)
# we have some of the information, return it in an interpolated string
return format_html(
'<strong>{main_string}{bio_dates}'
'</strong>{mep_id}<br /> {note_string}'.strip(),
**labels
)
[docs] def get_queryset(self):
''':class:`~mep.people.models.Person` queryset, filtered on
text in name or MEP id (case-insensitive, partial match)'''
return Person.objects.filter(
Q(name__icontains=self.q) |
Q(mep_id__icontains=self.q)
)
[docs]class CountryAutocomplete(autocomplete.Select2QuerySetView):
'''Basic autocomplete lookup, for use with django-autocomplete-light and
:class:`mep.people.models.Person` in nationalities many-to-many.
'''
[docs] def get_queryset(self):
''':class:`~mep.people.models.Country` queryset, filtered on
text in name (case-insensitive, partial match)'''
return Country.objects.filter(name__icontains=self.q)
[docs]class LocationAutocomplete(autocomplete.Select2QuerySetView):
'''Basic autocomplete lookup, for use with django-autocomplete-light and
:class:`mep.people.models.Person` in address many-to-many'''
[docs] def get_queryset(self):
'''
Get queryset of :class:`mep.people.models.Location` objects.
Use Q objects to search all relevant fields in autocomplete.
'''
# not searching lat or lon for now
return Location.objects.filter(
Q(name__icontains=self.q) |
Q(street_address__icontains=self.q) |
Q(city__icontains=self.q) |
Q(postal_code__icontains=self.q) |
Q(country__name__icontains=self.q) |
Q(address__person__name__icontains=self.q)
).order_by('name', 'city', 'street_address')
[docs]class PersonMerge(PermissionRequiredMixin, FormView):
'''View method to merge one or more :class:`~mep.people.models.Person`
records. Displays :class:`~mep.people.models.PersonMergeForm` on
GET, processes merge with :meth:`mep.people.models.PersonQuerySet.merge_with`
on successful POST. Should be called with a list of person ids
in the querystring as a comma-separated list. Created for use
with custom admin action
:meth:`mep.people.admin.PersonAdmin.merge_people`.
'''
permission_required = ('people.change_person', 'people.delete_person')
form_class = PersonMergeForm
template_name = 'people/merge_person.html'
[docs] def get_success_url(self):
'''
Redirect to the :class:`mep.people.models.Person` change_list in the
Django admin with pagination and filters preserved.
Expects :meth:`mep.people.admin.PersonAdmin.merge_people`
to have set 'people_merge_filter' in the request's session.
'''
change_list = reverse('admin:people_person_changelist')
# get request.session's querystring filter, if it exists,
# use rstrip to remove the ? so that we're left with an empty string
# otherwise
querystring = ('?%s' %
self.request.session.
get('people_merge_filter', '')).rstrip('?')
return '%s%s' % (change_list, querystring)
[docs] def get_initial(self):
# default to first person selected (?)
# _could_ add logic to select most complete record,
# but probably better for team members to choose
person_ids = self.request.GET.get('ids', None)
if person_ids:
self.person_ids = [int(pid) for pid in person_ids.split(',')]
# by default, prefer the first record created
return {'primary_person': sorted(self.person_ids)[0]}
self.person_ids = []