Princeton-CDH / mep-django

@@ -4,6 +4,7 @@
Loading
4 4
from dateutil.relativedelta import relativedelta
5 5
from django import forms
6 6
from django.contrib import admin
7 +
from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
7 8
from django.core.validators import RegexValidator
8 9
9 10
from mep.accounts.partial_date import PartialDateFormMixin
@@ -63,6 +64,24 @@
Loading
63 64
    extra = 1
64 65
65 66
67 +
class SubEventFootnoteFormset(BaseGenericInlineFormSet):
68 +
    ''''Custom inline formset for footnotes on non-geheric event classes.
69 +
    Find footnotes relative to the generic event instead of the subtable.
70 +
    '''
71 +
72 +
    def __init__(self, instance=None, *args, **kwargs):
73 +
        if instance and hasattr(instance, 'event_ptr'):
74 +
            # use base event object as model instance for retrieving footnotes
75 +
            instance = instance.event_ptr
76 +
77 +
        return super().__init__(instance=instance, *args, **kwargs)
78 +
79 +
80 +
class SubEventFootnoteInline(OpenFootnoteInline):
81 +
    '''footnote line variant for subclasses of Events'''
82 +
    formset = SubEventFootnoteFormset
83 +
84 +
66 85
class EventEditionFormMixin:
67 86
68 87
    def __init__(self, *args, **kwargs):
@@ -241,7 +260,7 @@
Loading
241 260
              'deposit', 'price_paid',
242 261
              'currency', 'notes')
243 262
    readonly_fields = ('duration',)
244 -
263 +
    inlines = (SubEventFootnoteInline, )
245 264
246 265
class SubscriptionInline(CollapsibleTabularInline):
247 266
    model = Subscription
@@ -281,13 +300,15 @@
Loading
281 300
    list_filter = ('currency',)
282 301
    search_fields = ('account__persons__name', 'account__persons__mep_id',
283 302
                     'notes')
303 +
    inlines = (SubEventFootnoteInline, )
284 304
285 305
286 306
class PurchaseAdminForm(PartialDateFormMixin):
287 307
288 308
    class Meta:
289 309
        model = Purchase
290 -
        fields = ('account', 'work', 'partial_start_date', 'price', 'currency', 'notes')
310 +
        fields = ('account', 'work', 'partial_start_date', 'price', 'currency',
311 +
                  'notes')
291 312
        help_texts = {
292 313
            'account': ('Searches and displays on system assigned '
293 314
                        'account id, as well as associated person and '
@@ -318,7 +339,7 @@
Loading
318 339
    list_filter = ('currency',)
319 340
    search_fields = ('account__persons__name', 'account__persons__mep_id',
320 341
                     'notes')
321 -
    inlines = [OpenFootnoteInline]
342 +
    inlines = (SubEventFootnoteInline, )
322 343
323 344
324 345
class PurchaseInline(CollapsibleTabularInline):
@@ -405,7 +426,7 @@
Loading
405 426
    class Meta:
406 427
        model = Borrow
407 428
        fields = ('account', 'work', 'item_status', 'partial_start_date',
408 -
            'partial_end_date', 'notes')
429 +
                  'partial_end_date', 'notes')
409 430
        widgets = {
410 431
            'account': AUTOCOMPLETE['account'],
411 432
            'work': AUTOCOMPLETE['work'],
@@ -439,7 +460,7 @@
Loading
439 460
        ('partial_start_date', 'partial_end_date'),
440 461
        ('notes')
441 462
    )
442 -
    inlines = (OpenFootnoteInline, )
463 +
    inlines = (SubEventFootnoteInline, )
443 464
444 465
    class Media:
445 466
        js = ['admin/borrow-admin-list.js']

@@ -88,8 +88,6 @@
Loading
88 88
            data['borrow'] = {
89 89
                'status': obj.borrow.get_item_status_display()
90 90
            }
91 -
            # capture a footnote if there is one
92 -
            footnote = obj.borrow.footnotes.first()
93 91
94 92
        # purchase data
95 93
        elif event_type == 'Purchase' and obj.purchase.price:
@@ -97,10 +95,9 @@
Loading
97 95
                'price': '%s%.2f' %
98 96
                         (obj.purchase.currency_symbol(), obj.purchase.price)
99 97
            }
100 -
            footnote = obj.purchase.footnotes.first()
101 98
102 -
        # check for footnote on the generic event if one was not already found
103 -
        footnote = footnote or obj.event_footnotes.first()
99 +
        # footnote should always be attached to the base event
100 +
        footnote = obj.footnotes.first()
104 101
105 102
        item_info = self.item_info(obj)
106 103
        if item_info:

@@ -1,4 +1,4 @@
Loading
1 -
__version_info__ = (1, 1, 0, None)
1 +
__version_info__ = (1, 1, 1, None)
2 2
3 3
4 4
# Dot-connect all but the last. Last is dash-connected if not None.

@@ -0,0 +1,45 @@
Loading
1 +
# Generated by Django 2.2.11 on 2020-05-04 18:09
2 +
3 +
from django.db import migrations
4 +
5 +
6 +
def consolidate_event_footnotes(apps, schema_editor):
7 +
    Footnote = apps.get_model('footnotes', 'Footnote')
8 +
    ContentType = apps.get_model('contenttypes', 'ContentType')
9 +
    Event = apps.get_model('accounts', 'Event')
10 +
11 +
    # get content types for the event models
12 +
    event_ctype = ContentType.objects.get(app_label='accounts', model='Event')
13 +
    borrow_ctype = ContentType.objects.get(app_label='accounts',
14 +
                                           model='Borrow')
15 +
    purchase_ctype = ContentType.objects.get(app_label='accounts',
16 +
                                             model='Purchase')
17 +
18 +
    # update all footnotes linked to borrows to event content type
19 +
    # and event id for associated borrow
20 +
    for fn in Footnote.objects.filter(content_type=borrow_ctype):
21 +
        fn.content_type = event_ctype
22 +
        event = Event.objects.get(borrow__pk=fn.object_id)
23 +
        fn.object_id = event.pk
24 +
        fn.save()
25 +
26 +
    # same for footnotes on purchases
27 +
    for fn in Footnote.objects.filter(content_type=purchase_ctype):
28 +
        fn.content_type = event_ctype
29 +
        event = Event.objects.get(purchase__pk=fn.object_id)
30 +
        fn.object_id = event.pk
31 +
        fn.save()
32 +
33 +
34 +
class Migration(migrations.Migration):
35 +
36 +
    dependencies = [
37 +
        ('footnotes', '0004_on_delete'),
38 +
    ]
39 +
40 +
    operations = [
41 +
        migrations.RunPython(
42 +
            code=consolidate_event_footnotes,
43 +
            reverse_code=migrations.RunPython.noop,
44 +
        ),
45 +
    ]

@@ -505,8 +505,8 @@
Loading
505 505
506 506
    def test_source_info(self):
507 507
        # footnote
508 -
        event = Event.objects.filter(event_footnotes__isnull=False).first()
509 -
        footnote = event.event_footnotes.first()
508 +
        event = Event.objects.filter(footnotes__isnull=False).first()
509 +
        footnote = event.footnotes.first()
510 510
        info = self.cmd.source_info(footnote)
511 511
        assert info['citation'] == footnote.bibliography.bibliographic_note
512 512
        assert info['manifest'] == footnote.bibliography.manifest.uri

@@ -308,32 +308,19 @@
Loading
308 308
            content_type__model__in=['event', 'borrow', 'purchase'])
309 309
310 310
    def events(self):
311 -
        '''Return an Events queryset of any events (including borrows and
312 -
        purchases) associated with the current footnote queryset.'''
311 +
        '''Return an Events queryset for all events
312 +
        associated with the current footnote queryset.'''
313 313
314 314
        # use get model to avoid circular import
315 315
        Event = apps.get_model('accounts', 'Event')
316 -
317 -
        # get event ids and content types from the current footnote queryset
318 -
        event_refs = self.on_events() \
319 -
                         .values('object_id', 'content_type__model')
320 -
        # group event ids by content type
321 -
        event_ids_by_type = defaultdict(list)
322 -
        for ref in event_refs:
323 -
            event_ids_by_type[ref['content_type__model']].append(ref['object_id'])
324 -
        # construct an OR filter query for each content type and list of ids
325 -
        # - look for nothing OR for events and event subtypes by id
326 -
        filter_q = models.Q(pk__in=[])
327 -
        for ctype, pk_list in event_ids_by_type.items():
328 -
            if ctype == 'borrow':
329 -
                filter_q |= models.Q(borrow__pk__in=pk_list)
330 -
            elif ctype == 'purchase':
331 -
                filter_q |= models.Q(purchase__pk__in=pk_list)
332 -
            elif ctype == 'event':
333 -
                filter_q |= models.Q(pk__in=pk_list)
316 +
        # get Event content type
317 +
        event_content_type = ContentType.objects.get_for_model(Event)
318 +
        # get a list of event ids from the current footnote queryset
319 +
        event_ids = self.filter(content_type=event_content_type) \
320 +
                        .values_list('object_id', flat=True)
334 321
335 322
        # find and return corresponding events
336 -
        return Event.objects.filter(filter_q)
323 +
        return Event.objects.filter(pk__in=event_ids)
337 324
338 325
    def event_date_range(self):
339 326
        '''Find earliest and latest dates for any events associated
@@ -371,13 +358,13 @@
Loading
371 358
        # and models that are available in django admin
372 359
        # (otherwise, lookup is not possible)
373 360
        # TODO: add items here as the application expands
374 -
        limit_choices_to=models.Q(app_label='people',
375 -
            model__in=['country', 'person', 'address', 'profession']) |
361 +
        limit_choices_to=models.Q(
362 +
                app_label='people',
363 +
                model__in=['country', 'person', 'address', 'profession']) |
376 364
            models.Q(
377 365
                app_label='accounts',
378 -
                model__in=['account', 'event', 'subscription', 'borrow',
379 -
                           'reimbursement', 'purchase']) |
380 -
            models.Q(app_label='books', model__in=['item'])
366 +
                model__in=['account', 'event']) |
367 +
            models.Q(app_label='books', model__in=['work'])
381 368
    )
382 369
    object_id = models.PositiveIntegerField()
383 370
    content_object = GenericForeignKey('content_type', 'object_id')

@@ -595,7 +595,7 @@
Loading
595 595
    # language model foreign key may be added in future
596 596
597 597
    class Meta:
598 -
        ordering = ['date']
598 +
        ordering = ['date', 'volume']
599 599
600 600
    def __repr__(self):
601 601
        # provide pk for easy lookup and string for recognition

@@ -461,7 +461,7 @@
Loading
461 461
        # retrieves and stores work
462 462
        assert self.view.work == self.work
463 463
        work_borrow = self.work.event_set.first().borrow
464 -
        work_footnotes = work_borrow.footnotes.all()
464 +
        work_footnotes = work_borrow.event_ptr.footnotes.all()
465 465
466 466
        # finds footnotes for this work with images and de-dupes based on image
467 467
        assert list(queryset) == list(work_footnotes)
@@ -521,7 +521,7 @@
Loading
521 521
        response = self.client.get(url)
522 522
523 523
        work_borrow = self.work.event_set.first().borrow
524 -
        work_footnote = work_borrow.footnotes.first()
524 +
        work_footnote = work_borrow.event_ptr.footnotes.first()
525 525
        # image included in two sizes
526 526
        self.assertContains(
527 527
            response, work_footnote.image.image.size(width=225))

@@ -417,7 +417,7 @@
Loading
417 417
            content_object=event,
418 418
            # object_id=event, content_type=event_ctype,
419 419
            image=other_img, bibliography=card)
420 -
        event.event_footnotes.add(fn)
420 +
        event.footnotes.add(fn)
421 421
        card_images = account.member_card_images()
422 422
        assert other_img in card_images
423 423
        # should come after main card images

@@ -25,5 +25,7 @@
Loading
25 25
        'name_t': 'name_ngram',
26 26
        'sort_name_t': 'name_ngram',
27 27
        # stemmed version of titles for searching
28 -
        'title_t': 'title_txt_en',
28 +
        'title_t': ['title_txt_ens', 'title_txt_en_nostem'],
29 +
        # author names without stop words
30 +
        'authors_t': 'authors_txt_en_nostem'
29 31
    }

@@ -240,17 +240,18 @@
Loading
240 240
            description = 'By %s' % ','.join(
241 241
                [a.name for a in self.object.authors])
242 242
        if self.object.year:
243 -
            description += ', %s.\n ' % self.object.year
244 -
        if self.object.public_notes:
245 -
            description += self.object.public_notes
243 +
            description += ', %s' % self.object.year
244 +
        description += '. '
246 245
247 246
        # text-only readable version of membership years for meta description
248 247
        circ_years = strip_tags(as_ranges(self.object.event_years)
249 248
                                .replace('</span>', ',')).rstrip(',')
250 249
251 -
        description += '%d event%s in %s.' % \
250 +
        description += '%d event%s in %s. ' % \
252 251
            (self.object.event_count,
253 252
             '' if self.object.event_count == 1 else 's', circ_years)
253 +
        if self.object.public_notes:
254 +
            description += self.object.public_notes
254 255
255 256
        context.update({
256 257
            'page_title': self.object.title,
@@ -260,8 +261,8 @@
Loading
260 261
261 262
262 263
class WorkCirculation(WorkLastModifiedListMixin, ListView, RdfViewMixin):
263 -
    '''Display a list of circulation events (borrows, purchases) for an
264 -
    individual work.'''
264 +
    '''Display a list of circulation events (borrows, purchases, etc)
265 +
    for an individual work.'''
265 266
    model = Event
266 267
    template_name = 'books/circulation.html'
267 268
@@ -312,23 +313,14 @@
Loading
312 313
        # that have images
313 314
        return super().get_queryset() \
314 315
                      .on_events() \
315 -
                      .filter(Q(borrows__work__pk=self.work.pk) |
316 -
                              Q(events__work__pk=self.work.pk) |
317 -
                              Q(purchases__work__pk=self.work.pk)) \
316 +
                      .filter(events__work__pk=self.work.pk) \
318 317
                      .filter(image__isnull=False) \
319 -
                      .annotate(date=Coalesce('borrows__start_date',
320 -
                                              'events__start_date',
321 -
                                              'purchases__start_date'),
322 -
                                start_date_precision=Coalesce(
323 -
                                    'borrows__start_date_precision',
324 -
                                    'events__start_date_precision',
325 -
                                    'purchases__start_date_precision')) \
326 318
                      .prefetch_related('content_object', 'image') \
327 -
                      .order_by(F('start_date_precision').desc(),
328 -
                                F('date').asc(nulls_last=True))
319 +
                      .order_by(F('events__start_date_precision').desc(),
320 +
                                F('events__start_date').asc(nulls_last=True))
329 321
330 322
        # NOTE: sorting by date precision decending (with default nulls first)
331 -
        # so that full precision dates (null or 7) come before partiald ates
323 +
        # so that full precision dates (null or 7) come before partial dates
332 324
333 325
    def get_absolute_url(self):
334 326
        '''Full URI for work card list page.'''

@@ -118,7 +118,7 @@
Loading
118 118
        '''Annotate the queryset with the number of events, borrows,
119 119
        and purchases for sorting and display.'''
120 120
        return super(WorkAdmin, self) \
121 -
            .get_queryset(request).count_events()
121 +
            .get_queryset(request).count_events().order_by('sort_title')
122 122
123 123
    def get_search_results(self, request, queryset, search_term):
124 124
        '''Override admin search to use Solr.'''

@@ -142,9 +142,9 @@
Loading
142 142
        assert not Footnote.objects.exclude(**gstein_filter).events().exists()
143 143
144 144
        # get a known date from the fixture
145 -
        evt = Borrow.objects.get(pk=26344)
145 +
        evt = Event.objects.get(pk=26344)
146 146
        assert evt.footnotes.all().events().count() == 1
147 -
        assert evt.footnotes.all().events().first() == evt.event_ptr
147 +
        assert evt.footnotes.all().events().first() == evt
148 148
149 149
        # all events from one account
150 150
        acct = Account.objects.first()  # currently only g. stein in fixture
@@ -178,7 +178,7 @@
Loading
178 178
            .event_date_range()
179 179
180 180
        # get a known date from the fixture with start and end date fully known
181 -
        evt = Borrow.objects.get(pk=26344)
181 +
        evt = Event.objects.get(pk=26344)
182 182
        footnote_dates = evt.footnotes.all().event_date_range()
183 183
184 184
        # should not be null
@@ -194,25 +194,16 @@
Loading
194 194
        assert footnote_dates[0] == event_dates[0]
195 195
        assert footnote_dates[1] == event_dates[-1]
196 196
197 -
        # add new purchase and generic events with footnotes
198 -
        generic_event = Event.objects.create(
197 +
        # add new event with footnotes
198 +
        event = Event.objects.create(
199 199
            account=evt.account, start_date=date(1919, 11, 17))
200 200
        evt_ctype = ContentType.objects.get_for_model(Event)
201 201
        bib = evt.footnotes.first().bibliography
202 -
        Footnote.objects.create(object_id=generic_event.pk,
202 +
        Footnote.objects.create(object_id=event.pk,
203 203
                                content_type=evt_ctype, bibliography=bib)
204 204
        footnote_dates = Footnote.objects.filter(**gstein_filter) \
205 205
            .event_date_range()
206 -
        assert footnote_dates[0] == generic_event.start_date
207 -
208 -
        purchase_evt = Purchase.objects.create(
209 -
            account=evt.account, start_date=date(1962, 3, 5))
210 -
        evt_ctype = ContentType.objects.get_for_model(Purchase)
211 -
        Footnote.objects.create(object_id=purchase_evt.pk,
212 -
                                content_type=evt_ctype, bibliography=bib)
213 -
        footnote_dates = Footnote.objects.filter(**gstein_filter) \
214 -
            .event_date_range()
215 -
        assert footnote_dates[1] == purchase_evt.start_date
206 +
        assert footnote_dates[0] == event.start_date
216 207
217 208
218 209
class TestBibliographyAdmin:

@@ -171,10 +171,7 @@
Loading
171 171
        # find all canvas associated with events for this account via footnote
172 172
        # (excluding those already in the manifest)
173 173
        event_cards = Canvas.objects.exclude(manifest=self.card.manifest) \
174 -
            .filter(
175 -
                models.Q(footnote__events__account__pk=self.pk) |
176 -
                models.Q(footnote__borrows__account__pk=self.pk) |
177 -
                models.Q(footnote__purchases__account__pk=self.pk)) \
174 +
            .filter(footnote__events__account__pk=self.pk) \
178 175
            .annotate(priority=models.Value('2', output_field=models.IntegerField())) \
179 176
            .distinct()
180 177
@@ -294,7 +291,7 @@
Loading
294 291
        help_text='Edition of the work, if known.',
295 292
        on_delete=models.deletion.SET_NULL)
296 293
297 -
    event_footnotes = GenericRelation(Footnote, related_query_name='events')
294 +
    footnotes = GenericRelation(Footnote, related_query_name='events')
298 295
299 296
    objects = EventQuerySet.as_manager()
300 297
@@ -336,7 +333,8 @@
Loading
336 333
        'NOTATION: BOUGHTFOR': 'Purchase',
337 334
        'NOTATION: SOLDFOR': 'Purchase',
338 335
        'NOTATION: REQUEST': 'Request',
339 -
        'STRIKETHRU': 'Crossed out'
336 +
        'STRIKETHRU': 'Crossed out',
337 +
        'NOTATION: PERIODICALSUBSCRIPTION': 'Periodical Subscription'
340 338
    }
341 339
    re_nonstandard_notation = re.compile(
342 340
        '(%s)' % '|'.join(nonstandard_notation.keys()))
@@ -534,7 +532,6 @@
Loading
534 532
        max_length=2, blank=True,
535 533
        help_text='Status of borrowed item (bought, missing, returned)',
536 534
        choices=STATUS_CHOICES)
537 -
    footnotes = GenericRelation(Footnote, related_query_name='borrows')
538 535
539 536
    def save(self, *args, **kwargs):
540 537
        # if end date is set and item status is not, automatically set
@@ -548,7 +545,6 @@
Loading
548 545
    '''Inherited table indicating purchase events; extends :class:`Event`'''
549 546
    price = models.DecimalField(max_digits=8, decimal_places=2,
550 547
                                blank=True, null=True)
551 -
    footnotes = GenericRelation(Footnote, related_query_name='purchases')
552 548
553 549
    def date(self):
554 550
        '''alias of :attr:`date_range` for display; since reimbersument
Files Coverage
mep 98.40%
srcmedia/ts 86.55%
Project Totals (219 files) 97.95%
2404.1
TRAVIS_PYTHON_VERSION=3.5
TRAVIS_OS_NAME=linux
2398.1
TRAVIS_PYTHON_VERSION=3.5
TRAVIS_OS_NAME=linux
2433.1
TRAVIS_PYTHON_VERSION=3.5
TRAVIS_OS_NAME=linux
2413.1
TRAVIS_PYTHON_VERSION=3.5
TRAVIS_OS_NAME=linux

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading