import logging
from collections import defaultdict
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.functions import Coalesce
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from djiffy.models import Canvas, Manifest
from parasolr.django.indexing import ModelIndexable
from mep.common.models import Named, Notable
logger = logging.getLogger(__name__)
[docs]class BibliographySignalHandlers:
'''Signal handlers for indexing :class:`Bibliography` records when
related records are saved or deleted.'''
[docs] @staticmethod
def debug_log(name, count, mode='save'):
'''shared debug logging for card signal save handlers'''
logger.debug('%s %s, reindexing %d related card%s',
mode, name, count, '' if count == 1 else 's')
[docs] @staticmethod
def person_save(sender=None, instance=None, raw=False, **kwargs):
'''when a person is saved, reindex bibliography card records
associated through an account'''
# raw = saved as presented; don't query the database
if raw or not instance.pk:
return
# find any cards associated via an account
cards = Bibliography.objects.filter(account__persons__pk=instance.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('person', cards.count())
ModelIndexable.index_items(cards)
[docs] @staticmethod
def person_delete(sender, instance, **kwargs):
'''when a person is deleted, reindex any bibliography card
records associated through an account'''
card_ids = Bibliography.objects \
.filter(account__persons__pk=instance.pk) \
.values_list('id', flat=True)
if card_ids:
# find the items based on the list of ids to reindex
cards = Bibliography.objects.filter(id__in=list(card_ids))
# clear the assocation so items will index without this person
instance.account_set.clear()
BibliographySignalHandlers.debug_log('person', cards.count(),
mode='delete')
ModelIndexable.index_items(cards)
[docs] @staticmethod
def account_save(sender=None, instance=None, raw=False, **_kwargs):
'''when an account is saved, reindex any associated library
lending card.'''
# raw = saved as presented; don't query the database
if raw or not instance.pk:
return
# find any cards associated with this account
cards = Bibliography.objects.filter(account__pk=instance.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('account', cards.count())
ModelIndexable.index_items(cards)
[docs] @staticmethod
def account_delete(sender, instance, **kwargs):
'''when an account is deleted, reindex any associated library
lending card'''
card_ids = Bibliography.objects.filter(account__pk=instance.pk) \
.values_list('id', flat=True)
if card_ids:
# delete the assocation so cards will index without the account
instance.card = None
instance.save()
# find the items based on the list of ids to reindex
cards = Bibliography.objects.filter(id__in=list(card_ids))
BibliographySignalHandlers.debug_log('account', cards.count(),
mode='delete')
ModelIndexable.index_items(cards)
[docs] @staticmethod
def manifest_save(sender=None, instance=None, raw=False, **kwargs):
'''when a manifest is saved, reindex associated library
lending card'''
# raw = saved as presented; don't query the database
if raw or not instance.pk:
return
# find any cards associated with this account
cards = Bibliography.objects.filter(manifest__pk=instance.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('manifest', cards.count())
ModelIndexable.index_items(cards)
[docs] @staticmethod
def manifest_delete(sender, instance, **kwargs):
'''when a manifest is deleted, reindex associated library
lending card'''
card_ids = Bibliography.objects.filter(manifest__pk=instance.pk) \
.values_list('id', flat=True)
if card_ids:
# delete the assocation so cards will index without the account
instance.bibliography_set.clear()
# find the items based on the list of ids to reindex
cards = Bibliography.objects.filter(id__in=list(card_ids))
BibliographySignalHandlers.debug_log('manifest', cards.count(),
mode='delete')
ModelIndexable.index_items(cards)
[docs] @staticmethod
def canvas_save(sender=None, instance=None, raw=False, **kwargs):
'''when a canvas is saved, reindex library lending card
associated via manifest'''
# raw = saved as presented; don't query the database
if raw or not instance.pk:
return
# find any cards associated with this canvas, via manifest
cards = Bibliography.objects.filter(manifest__pk=instance.manifest.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('canvas', cards.count())
ModelIndexable.index_items(cards)
[docs] @staticmethod
def canvas_delete(sender, instance, **kwargs):
'''when a canvas is deleted, reindex library lending card
associated via manifest'''
cards = Bibliography.objects.filter(manifest__pk=instance.manifest.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('canvas', cards.count(),
mode='delete')
ModelIndexable.index_items(cards)
[docs] @staticmethod
def event_save(sender=None, instance=None, raw=False, **_kwargs):
'''when an event is saved, reindex library lending card
associated via account'''
# NOTE: should this also/instead rely on footnote associatio?
# raw = saved as presented; don't query the database
if raw or not instance.pk:
return
# find any cards associated with this event, via account
cards = Bibliography.objects.filter(account__pk=instance.account.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('event', cards.count())
ModelIndexable.index_items(cards)
[docs] @staticmethod
def event_delete(sender, instance, **kwargs):
'''when an event is deleted, reindex library lending card
associated via account'''
cards = Bibliography.objects.filter(account__pk=instance.account.pk)
if cards.exists():
BibliographySignalHandlers.debug_log('event', cards.count(),
mode='delete')
ModelIndexable.index_items(cards)
[docs]class SourceType(Named, Notable):
'''Type of source document.'''
[docs] def item_count(self):
'''number of associated bibliographic items'''
return self.bibliography_set.count()
item_count.short_description = '# items'
[docs]class Bibliography(Notable, ModelIndexable):
# Note: citation might be better singular
bibliographic_note = models.TextField(
help_text='Full bibliographic citation')
source_type = models.ForeignKey(SourceType,
on_delete=models.CASCADE)
#: digital version as instance of :class:`djiffy.models.Manifest`
manifest = models.ForeignKey(
Manifest, blank=True, null=True, on_delete=models.SET_NULL,
help_text='Digitized version of lending card, if locally available')
class Meta:
verbose_name_plural = 'Bibliographies'
ordering = ('bibliographic_note',)
def __str__(self):
return self.bibliographic_note
footnote_count.short_description = '# footnotes'
[docs] @classmethod
def index_item_type(cls):
"""Label for this kind of indexable item."""
# override default behavior (using model verbose name)
# since we are only care about indexing cards, and not
# all bibliography records
return 'card'
[docs] @classmethod
def items_to_index(cls):
'''Custom logic for finding items for bulk indexing; only include
records associated with an account and with a IIIF manifest.'''
return cls.objects.filter(account__isnull=False,
manifest__isnull=False)
index_depends_on = {
'account_set': {
'post_save': BibliographySignalHandlers.account_save,
'pre_delete': BibliographySignalHandlers.account_delete
},
'account_set__persons': {
'post_save': BibliographySignalHandlers.person_save,
'pre_delete': BibliographySignalHandlers.person_delete
},
# NOTE: using app.Model notation here because
# parasolr doesn't currently support foreignkey relation lookup
'djiffy.Manifest': {
'post_save': BibliographySignalHandlers.manifest_save,
'pre_delete': BibliographySignalHandlers.manifest_delete
},
'djiffy.Canvas': {
'post_save': BibliographySignalHandlers.canvas_save,
'post_delete': BibliographySignalHandlers.canvas_delete
},
'accounts.Event': {
'post_save': BibliographySignalHandlers.event_save,
'post_delete': BibliographySignalHandlers.event_delete,
},
# unfortunately the generic event signals aren't fired
# when subclass types are edited directly, so bind the same signal
'accounts.Borrow': {
'post_save': BibliographySignalHandlers.event_save,
'post_delete': BibliographySignalHandlers.event_delete,
},
'accounts.Purchase': {
'post_save': BibliographySignalHandlers.event_save,
'post_delete': BibliographySignalHandlers.event_delete,
},
'accounts.Subscription': {
'post_save': BibliographySignalHandlers.event_save,
'post_delete': BibliographySignalHandlers.event_delete,
},
'accounts.Reimbursement': {
'post_save': BibliographySignalHandlers.event_save,
'post_delete': BibliographySignalHandlers.event_delete,
}
}
[docs] def index_data(self):
'''data for indexing in Solr'''
index_data = super().index_data()
# only library lending cards are indexed; if bibliography
# does not have a manifest or is not associated with an account,
# return id only.
# This will blank out any previously indexed values, and item
# will not be findable by any public searchable fields.
account = self.account_set.all().first()
if not self.manifest or not self.account_set.all().exists():
del index_data['item_type']
return index_data
# we expect a thumbnail, but possible there is none
if self.manifest.thumbnail:
iiif_thumbnail = self.manifest.thumbnail.image
# for now, store iiif thumbnail urls directly
index_data['thumbnail_t'] = str(iiif_thumbnail.size(width=225))
index_data['thumbnail2x_t'] = str(iiif_thumbnail.size(width=225 * 2))
names = []
account_years = set()
for account in self.account_set.all():
for person in account.persons.all():
names.append(person.sort_name)
account_years.update(set(date.year for date in
account.event_dates))
if names:
index_data.update({
'cardholder_t': names,
'cardholder_sort_s': names[0],
})
if account_years:
index_data.update({
'years_is': list(account_years),
'start_i': min(account_years),
'end_i': max(account_years),
})
return index_data