0001-journal-permit-custom-prefetching-51808.patch
src/authentic2/apps/journal/forms.py | ||
---|---|---|
330 | 330 |
first = len(page) <= limit |
331 | 331 |
last = True |
332 | 332 |
page = page[-limit:] |
333 |
models.prefetch_events_references(page) |
|
333 |
models.prefetch_events_references(page, prefetcher=self.prefetcher)
|
|
334 | 334 |
if page: |
335 | 335 |
self.data = self.data.copy() |
336 | 336 |
self.cleaned_data['after_cursor'] = self.data['after_cursor'] = page[0].cursor.minus_one() |
337 | 337 |
self.cleaned_data['before_cursor'] = '' |
338 | 338 |
return Page(self, page, first, last) |
339 | 339 | |
340 |
def prefetcher(self, model, pks): |
|
341 |
return [] |
|
342 | ||
340 | 343 |
@cached_property |
341 | 344 |
def date_hierarchy(self): |
342 | 345 |
self.is_valid() |
src/authentic2/apps/journal/models.py | ||
---|---|---|
20 | 20 |
from contextlib import contextmanager |
21 | 21 |
from datetime import datetime, timedelta |
22 | 22 | |
23 |
import django |
|
23 | 24 |
from django.conf import settings |
24 | 25 |
from django.contrib.auth import get_user_model |
25 | 26 |
from django.contrib.contenttypes.models import ContentType |
... | ... | |
458 | 459 |
return EventCursor('%s %s' % (self.timestamp.timestamp(), self.event_id - 1)) |
459 | 460 | |
460 | 461 | |
461 |
def prefetch_events_references(events): |
|
462 |
def prefetch_events_references(events, prefetcher=None):
|
|
462 | 463 |
'''Prefetch references on an iterable of events, prevent N+1 queries problem.''' |
463 | 464 |
grouped_references = defaultdict(set) |
464 | 465 |
references = {} |
... | ... | |
473 | 474 |
content_type = ContentType.objects.get_for_id(content_type_id) |
474 | 475 |
for instance in content_type.get_all_objects_for_this_type(pk__in=instance_pks): |
475 | 476 |
references[(content_type_id, instance.pk)] = instance |
477 |
if prefetcher: |
|
478 |
deleted_pks = [pk for pk in instance_pks if (content_type_id, pk) not in references] |
|
479 |
if deleted_pks: |
|
480 |
for found_pk, instance in prefetcher(content_type.model_class(), deleted_pks): |
|
481 |
references[(content_type_id, found_pk)] = instance |
|
482 | ||
483 |
# prefetch the user column if absent |
|
484 |
if prefetcher: |
|
485 |
user_to_events = {} |
|
486 |
for event in events: |
|
487 |
if event.user is None and event.user_id: |
|
488 |
user_to_events.setdefault(event.user_id, []).append(event) |
|
489 |
for found_pk, instance in prefetcher(User, user_to_events.keys()): |
|
490 |
for event in user_to_events[found_pk]: |
|
491 |
# prevent TypeError in user's field descriptor __set__ method |
|
492 |
if django.VERSION < (2,): |
|
493 |
event._user_cache = instance |
|
494 |
else: |
|
495 |
event._state.fields_cache['user'] = instance |
|
476 | 496 | |
477 | 497 |
# assign references to events |
478 | 498 |
for event in events: |
tests/test_journal.py | ||
---|---|---|
28 | 28 |
from authentic2.a2_rbac.utils import get_default_ou |
29 | 29 |
from authentic2.apps.journal.forms import JournalForm |
30 | 30 |
from authentic2.apps.journal.journal import Journal |
31 |
from authentic2.apps.journal.models import Event, EventType, EventTypeDefinition, clean_registry |
|
31 |
from authentic2.apps.journal.models import ( |
|
32 |
Event, |
|
33 |
EventType, |
|
34 |
EventTypeDefinition, |
|
35 |
clean_registry, |
|
36 |
prefetch_events_references, |
|
37 |
) |
|
32 | 38 |
from authentic2.models import Service |
33 | 39 | |
34 | 40 |
User = get_user_model() |
... | ... | |
146 | 152 |
assert list(event.get_typed_references(User, None)) == [None, None] |
147 | 153 |
event = Event.objects.get() |
148 | 154 |
assert list(event.get_typed_references(Service, User)) == [None, None] |
155 |
assert event.user is None |
|
149 | 156 | |
150 | 157 | |
151 | 158 |
def test_event_types(clean_event_types_definition_registry): |
... | ... | |
669 | 676 |
ou_with_no_service = OU.objects.create(name='Second OU') |
670 | 677 |
stats = event_type_definition.get_method_statistics('month', services_ou=ou_with_no_service) |
671 | 678 |
assert stats == {'x_labels': [], 'series': []} |
679 | ||
680 | ||
681 |
def test_prefetcher(db): |
|
682 |
event_type = EventType.objects.get_for_name('user.login') |
|
683 |
for i in range(10): |
|
684 |
user = User.objects.create() |
|
685 |
Event.objects.create(type=event_type, user=user, references=[user]) |
|
686 |
Event.objects.create(type=event_type, user=user, references=[user]) |
|
687 | ||
688 |
User.objects.all().delete() |
|
689 | ||
690 |
events = list(Event.objects.all()) |
|
691 |
prefetch_events_references(events) |
|
692 |
for event in events: |
|
693 |
assert event.user is None |
|
694 |
assert list(event.get_typed_references(User)) == [None] |
|
695 | ||
696 |
def prefetcher(model, pks): |
|
697 |
if not issubclass(model, User): |
|
698 |
return |
|
699 |
for pk in pks: |
|
700 |
yield pk, 'deleted %s' % pk |
|
701 | ||
702 |
events = list(Event.objects.all()) |
|
703 |
prefetch_events_references(events, prefetcher=prefetcher) |
|
704 |
for event in events: |
|
705 |
s = 'deleted %s' % event.user_id |
|
706 |
assert event.user == s |
|
707 |
assert list(event.get_typed_references((str, User))) == [s] |
|
672 |
- |