add footnotes to document event sources in the address books and logbooks
Showing 4 of 4 files from the diff.
Newly tracked file
...ep/accounts/migrations/0034_add_event_footnotes.py
created.
Newly tracked file
...ep/accounts/migrations/0035_merge_20201210_1240.py
created.
@@ -0,0 +1,119 @@
Loading
1 | + | # Generated by Django 2.2.11 on 2020-12-08 14:53 |
|
2 | + | ||
3 | + | from django.db import migrations |
|
4 | + | from django.contrib.admin.models import ADDITION |
|
5 | + | from django.conf import settings |
|
6 | + | ||
7 | + | ||
8 | + | def add_missing_footnotes(apps, schema_editor): |
|
9 | + | ''' |
|
10 | + | Add footnotes to document source for events without footnotes. |
|
11 | + | Events should be associated with one of two address books based on |
|
12 | + | P36ADD and 36ADD tags in the event notes; all other events without |
|
13 | + | footnotes are from the logbooks. |
|
14 | + | ''' |
|
15 | + | Bibliography = apps.get_model('footnotes', 'Bibliography') |
|
16 | + | Footnote = apps.get_model('footnotes', 'Footnote') |
|
17 | + | Event = apps.get_model('accounts', 'Event') |
|
18 | + | ContentType = apps.get_model('contenttypes', 'ContentType') |
|
19 | + | User = apps.get_model('auth', 'User') |
|
20 | + | LogEntry = apps.get_model('admin', 'LogEntry') |
|
21 | + | ||
22 | + | event_content_type = ContentType.objects \ |
|
23 | + | .get(model='event', app_label='accounts') |
|
24 | + | footnote_content_type = ContentType.objects \ |
|
25 | + | .get(model='footnote', app_label='footnotes') |
|
26 | + | script_user = User.objects.get(username=settings.SCRIPT_USERNAME) |
|
27 | + | ||
28 | + | # get bibliographic entries to be used as sources for footnotes |
|
29 | + | # (but handle case where they don't exist, in a new db) |
|
30 | + | addressbook_1936 = Bibliography.objects \ |
|
31 | + | .filter(bibliographic_note__contains="Address Book 1919–1935", |
|
32 | + | source_type__name='Address Book').first() |
|
33 | + | addressbook_post1936 = Bibliography.objects \ |
|
34 | + | .filter(bibliographic_note__contains="Address Book 1935–1937", |
|
35 | + | source_type__name='Address Book').first() |
|
36 | + | logbooks = Bibliography.objects \ |
|
37 | + | .filter(bibliographic_note__contains="Logbooks 1919–1941", |
|
38 | + | source_type__name='Logbook').first() |
|
39 | + | ||
40 | + | # because footnote is a generic relation, it can't be used in a |
|
41 | + | # queryset filter in a migration; instead, get a list event ids |
|
42 | + | # with footnotes, so we can exclude them |
|
43 | + | events_with_footnotes = Footnote.objects \ |
|
44 | + | .filter(content_type=event_content_type) \ |
|
45 | + | .values_list('object_id') |
|
46 | + | ||
47 | + | if addressbook_post1936: |
|
48 | + | # find all events with tag P36ADD and no footnote |
|
49 | + | events_post36add = Event.objects.filter(notes__contains='P36ADD') \ |
|
50 | + | .exclude(pk__in=events_with_footnotes) |
|
51 | + | # for each event: create footnote, then create log entry for the footnote |
|
52 | + | # NOTE: not using bulk create because the objects it returns don't have pks |
|
53 | + | for event in events_post36add: |
|
54 | + | footnote = Footnote.objects.create( |
|
55 | + | bibliography=addressbook_post1936, |
|
56 | + | content_type=event_content_type, |
|
57 | + | object_id=event.pk) |
|
58 | + | LogEntry.objects.create( |
|
59 | + | user_id=script_user.id, |
|
60 | + | content_type_id=footnote_content_type.pk, |
|
61 | + | object_id=footnote.pk, |
|
62 | + | object_repr='Footnote on event %s for %s' % |
|
63 | + | (event.pk, addressbook_post1936.bibliographic_note), |
|
64 | + | change_message='Address book footnote created based on P36ADD tag', |
|
65 | + | action_flag=ADDITION) |
|
66 | + | ||
67 | + | if addressbook_1936: |
|
68 | + | # find events from the 1936 address book; exclude events from |
|
69 | + | # post-1936 address book or tha talready have |
|
70 | + | events_36add = Event.objects.filter(notes__contains='36ADD') \ |
|
71 | + | .exclude(notes__contains='P36ADD') \ |
|
72 | + | .exclude(pk__in=events_with_footnotes) |
|
73 | + | # for each event: create footnote, then create log entry for the footnote |
|
74 | + | for event in events_36add: |
|
75 | + | footnote = Footnote.objects.create( |
|
76 | + | bibliography=addressbook_1936, |
|
77 | + | content_type=event_content_type, |
|
78 | + | object_id=event.pk) |
|
79 | + | LogEntry.objects.create( |
|
80 | + | user_id=script_user.id, |
|
81 | + | content_type_id=footnote_content_type.pk, |
|
82 | + | object_id=footnote.pk, |
|
83 | + | object_repr='Footnote on event %s for %s' % |
|
84 | + | (event.pk, addressbook_1936.bibliographic_note), |
|
85 | + | change_message='Address book footnote created based on 36ADD tag', |
|
86 | + | action_flag=ADDITION) |
|
87 | + | ||
88 | + | if logbooks: |
|
89 | + | # find all remaining events without footnotes — from the logbooks |
|
90 | + | logbooks_events = Event.objects.exclude(notes__contains='36ADD') \ |
|
91 | + | .exclude(pk__in=events_with_footnotes) |
|
92 | + | for event in logbooks_events: |
|
93 | + | footnote = Footnote.objects.create( |
|
94 | + | bibliography=logbooks, |
|
95 | + | content_type=event_content_type, |
|
96 | + | object_id=event.pk) |
|
97 | + | LogEntry.objects.create( |
|
98 | + | user_id=script_user.id, |
|
99 | + | content_type_id=footnote_content_type.pk, |
|
100 | + | object_id=footnote.pk, |
|
101 | + | object_repr='Footnote on event %s for %s' % |
|
102 | + | (event.pk, logbooks.bibliographic_note), |
|
103 | + | change_message='Associated with logbooks', |
|
104 | + | action_flag=ADDITION) |
|
105 | + | ||
106 | + | ||
107 | + | class Migration(migrations.Migration): |
|
108 | + | ||
109 | + | dependencies = [ |
|
110 | + | ('accounts', '0033_subscription_purchase_date_adjustments'), |
|
111 | + | ('footnotes', '0005_consolidate_event_footnotes'), |
|
112 | + | ('admin', '0003_logentry_add_action_flag_choices'), |
|
113 | + | ('common', '0005_create_script_user') |
|
114 | + | ] |
|
115 | + | ||
116 | + | operations = [ |
|
117 | + | migrations.RunPython(add_missing_footnotes, |
|
118 | + | migrations.RunPython.noop) |
|
119 | + | ] |
@@ -1,5 +1,7 @@
Loading
1 | 1 | import datetime |
|
2 | 2 | ||
3 | + | from django.conf import settings |
|
4 | + | from django.contrib.admin.models import ADDITION |
|
3 | 5 | from django.db import connection |
|
4 | 6 | from django.db.migrations.executor import MigrationExecutor |
|
5 | 7 | from django.test import TransactionTestCase |
@@ -323,3 +325,103 @@
Loading
323 | 325 | assert account4_renew2.purchase_date == self.account4_renew2.start_date |
|
324 | 326 | assert account4_renew2.start_date == account4_renew.end_date |
|
325 | 327 | assert account4_renew2.end_date == datetime.date(1920, 8, 1) |
|
328 | + | ||
329 | + | ||
330 | + | @pytest.mark.last |
|
331 | + | class TestEventAddFootnotes(TestMigrations): |
|
332 | + | ||
333 | + | app = 'accounts' |
|
334 | + | migrate_from = '0033_subscription_purchase_date_adjustments' |
|
335 | + | migrate_to = '0034_add_event_footnotes' |
|
336 | + | ||
337 | + | def setUpBeforeMigration(self, apps): |
|
338 | + | ||
339 | + | Account = apps.get_model('accounts', 'Account') |
|
340 | + | Subscription = apps.get_model('accounts', 'Subscription') |
|
341 | + | SourceType = apps.get_model('footnotes', 'SourceType') |
|
342 | + | Bibliography = apps.get_model('footnotes', 'Bibliography') |
|
343 | + | Footnote = apps.get_model('footnotes', 'Footnote') |
|
344 | + | ContentType = apps.get_model('contenttypes', 'ContentType') |
|
345 | + | User = apps.get_model('auth', 'User') |
|
346 | + | ||
347 | + | event_content_type = ContentType.objects \ |
|
348 | + | .get(model='event', app_label='accounts') |
|
349 | + | ||
350 | + | # create script user |
|
351 | + | User.objects.get_or_create(username=settings.SCRIPT_USERNAME) |
|
352 | + | ||
353 | + | # create bibliography & source entries for migration |
|
354 | + | addressbook_source = SourceType.objects \ |
|
355 | + | .get_or_create(name='Address Book')[0] |
|
356 | + | logbook_source = SourceType.objects \ |
|
357 | + | .get_or_create(name='Logbook')[0] |
|
358 | + | self.addressbook_1936 = Bibliography.objects.create( |
|
359 | + | bibliographic_note="Address Book 1919–1935", |
|
360 | + | source_type=addressbook_source) |
|
361 | + | self.addressbook_post1936 = Bibliography.objects.create( |
|
362 | + | bibliographic_note="Address Book 1935–1937", |
|
363 | + | source_type=addressbook_source) |
|
364 | + | self.logbooks = Bibliography.objects.create( |
|
365 | + | bibliographic_note="Logbooks 1919–1941", |
|
366 | + | source_type=logbook_source) |
|
367 | + | ||
368 | + | # create Account to hold events |
|
369 | + | account = Account.objects.create() |
|
370 | + | ||
371 | + | # create sub with footnote |
|
372 | + | self.sub_with_note = Subscription.objects.create( |
|
373 | + | account=account, |
|
374 | + | start_date=datetime.date(1950, 1, 5)) |
|
375 | + | Footnote.objects.create( |
|
376 | + | bibliography=self.addressbook_1936, is_agree=True, |
|
377 | + | content_type=event_content_type, object_id=self.sub_with_note.pk) |
|
378 | + | # subs with P36ADD and 36ADD tags |
|
379 | + | self.sub_p36add_with_note = Subscription.objects.create( |
|
380 | + | account=account, notes='P36ADD', |
|
381 | + | start_date=datetime.date(1950, 2, 5)) |
|
382 | + | Footnote.objects.create( |
|
383 | + | bibliography=self.addressbook_post1936, is_agree=True, |
|
384 | + | content_type=event_content_type, object_id=self.sub_p36add_with_note.pk) |
|
385 | + | self.sub_p36add = Subscription.objects.create( |
|
386 | + | account=account, notes='P36ADD', |
|
387 | + | start_date=datetime.date(1950, 2, 15)) |
|
388 | + | self.sub_36add = Subscription.objects.create( |
|
389 | + | account=account, notes='36ADD', |
|
390 | + | start_date=datetime.date(1950, 3, 5)) |
|
391 | + | # sub with no tags or footnotes |
|
392 | + | self.sub_logbook = Subscription.objects.create( |
|
393 | + | account=account, |
|
394 | + | start_date=datetime.date(1950, 2, 5)) |
|
395 | + | ||
396 | + | def test_added_footnotes(self): |
|
397 | + | Footnote = self.apps.get_model('footnotes', 'Footnote') |
|
398 | + | ContentType = self.apps.get_model('contenttypes', 'ContentType') |
|
399 | + | LogEntry = self.apps.get_model('admin', 'LogEntry') |
|
400 | + | ||
401 | + | event_content_type = ContentType.objects \ |
|
402 | + | .get(model='event', app_label='accounts') |
|
403 | + | footnote_content_type = ContentType.objects \ |
|
404 | + | .get(model='footnote', app_label='footnotes') |
|
405 | + | ||
406 | + | # pre-filtered querysets for the following tests |
|
407 | + | event_footnotes = Footnote.objects.filter(content_type=event_content_type) |
|
408 | + | log_entries = LogEntry.objects.filter( |
|
409 | + | content_type_id=footnote_content_type, action_flag=ADDITION) |
|
410 | + | ||
411 | + | # should not add a second footnote |
|
412 | + | assert event_footnotes.filter(object_id=self.sub_with_note.pk) \ |
|
413 | + | .count() == 1 |
|
414 | + | # no second footnote even if there is a tag |
|
415 | + | assert event_footnotes.filter(object_id=self.sub_p36add_with_note.pk) \ |
|
416 | + | .count() == 1 |
|
417 | + | ||
418 | + | # new footnotes created with correct sources and log entries |
|
419 | + | sub_p36add_fn = event_footnotes.get(object_id=self.sub_p36add.pk) |
|
420 | + | assert sub_p36add_fn.bibliography.pk == self.addressbook_post1936.pk |
|
421 | + | assert log_entries.filter(object_id=sub_p36add_fn.pk).count() == 1 |
|
422 | + | sub_36add_fn = event_footnotes.get(object_id=self.sub_36add.pk) |
|
423 | + | assert sub_36add_fn.bibliography.pk == self.addressbook_1936.pk |
|
424 | + | assert log_entries.filter(object_id=sub_36add_fn.pk).count() == 1 |
|
425 | + | logbook_fn = event_footnotes.get(object_id=self.sub_logbook.pk) |
|
426 | + | assert logbook_fn.bibliography.pk == self.logbooks.pk |
|
427 | + | assert log_entries.filter(object_id=logbook_fn.pk).count() == 1 |
@@ -1,5 +1,6 @@
Loading
1 | 1 | # Generated by Django 2.2.11 on 2020-05-04 18:09 |
|
2 | 2 | ||
3 | + | from django.contrib.auth.management import create_permissions |
|
3 | 4 | from django.db import migrations |
|
4 | 5 | ||
5 | 6 |
@@ -8,12 +9,19 @@
Loading
8 | 9 | ContentType = apps.get_model('contenttypes', 'ContentType') |
|
9 | 10 | Event = apps.get_model('accounts', 'Event') |
|
10 | 11 | ||
12 | + | # run create permissions for all accounts apps to ensure |
|
13 | + | # that needed content types are created |
|
14 | + | accounts_app_config = apps.get_app_config('accounts') |
|
15 | + | accounts_app_config.models_module = True |
|
16 | + | create_permissions(accounts_app_config, apps=apps, verbosity=0) |
|
17 | + | accounts_app_config.models_module = None |
|
18 | + | ||
11 | 19 | # get content types for the event models |
|
12 | - | event_ctype = ContentType.objects.get(app_label='accounts', model='Event') |
|
20 | + | event_ctype = ContentType.objects.get(app_label='accounts', model='event') |
|
13 | 21 | borrow_ctype = ContentType.objects.get(app_label='accounts', |
|
14 | - | model='Borrow') |
|
22 | + | model='borrow') |
|
15 | 23 | purchase_ctype = ContentType.objects.get(app_label='accounts', |
|
16 | - | model='Purchase') |
|
24 | + | model='purchase') |
|
17 | 25 | ||
18 | 26 | # update all footnotes linked to borrows to event content type |
|
19 | 27 | # and event id for associated borrow |
@@ -35,6 +43,8 @@
Loading
35 | 43 | ||
36 | 44 | dependencies = [ |
|
37 | 45 | ('footnotes', '0004_on_delete'), |
|
46 | + | ('accounts', '0031_on_delete'), |
|
47 | + | ('contenttypes', '0002_remove_content_type_name') |
|
38 | 48 | ] |
|
39 | 49 | ||
40 | 50 | operations = [ |
@@ -0,0 +1,14 @@
Loading
1 | + | # Generated by Django 2.2.11 on 2020-12-10 17:40 |
|
2 | + | ||
3 | + | from django.db import migrations |
|
4 | + | ||
5 | + | ||
6 | + | class Migration(migrations.Migration): |
|
7 | + | ||
8 | + | dependencies = [ |
|
9 | + | ('accounts', '0034_subscription_other_to_separate_deposit'), |
|
10 | + | ('accounts', '0034_add_event_footnotes'), |
|
11 | + | ] |
|
12 | + | ||
13 | + | operations = [ |
|
14 | + | ] |
Files | Coverage |
---|---|
mep | 98.44% |
srcmedia/ts | 86.55% |
Project Totals (226 files) | 98.01% |
2541.1
TRAVIS_PYTHON_VERSION=3.5 TRAVIS_OS_NAME=linux
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.