Projet

Général

Profil

0001-journal-permit-custom-prefetching-51808.patch

Benjamin Dauvergne, 21 mai 2021 12:01

Télécharger (5,74 ko)

Voir les différences:

Subject: [PATCH 1/3] journal: permit custom prefetching (#51808)

 src/authentic2/apps/journal/forms.py  |  5 +++-
 src/authentic2/apps/journal/models.py | 22 +++++++++++++++-
 tests/test_journal.py                 | 38 ++++++++++++++++++++++++++-
 3 files changed, 62 insertions(+), 3 deletions(-)
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
-