Skip to content

Commit 21e7751

Browse files
committed
[ci skip] Add a method 'EntityCell.mixed_populate_entities()'.
1 parent 6aada06 commit 21e7751

File tree

5 files changed

+219
-103
lines changed

5 files changed

+219
-103
lines changed

creme/creme_core/core/entity_cell.py

+42-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@
1919
################################################################################
2020

2121
import logging
22-
from typing import Dict, Iterable, List, Optional, Tuple, Type # Callable
22+
from collections import defaultdict
23+
from typing import ( # Callable
24+
DefaultDict,
25+
Dict,
26+
Iterable,
27+
List,
28+
Optional,
29+
Sequence,
30+
Tuple,
31+
Type,
32+
)
2333

2434
from django.db import models
2535
from django.db.models import Field, FieldDoesNotExist, Model
@@ -179,7 +189,37 @@ def is_multiline(self) -> bool:
179189
return issubclass(self._get_field_class(), MULTILINE_FIELDS)
180190

181191
@staticmethod
182-
def populate_entities(cells, entities, user):
192+
def mixed_populate_entities(cells: Iterable['EntityCell'],
193+
entities: Sequence[CremeEntity],
194+
user) -> None:
195+
"""Fill caches of CremeEntity objects with grouped SQL queries, & so
196+
avoid multiple queries when rendering the cells.
197+
The given cells are grouped by types, and then the method
198+
'populate_entities()' of each used type is called.
199+
@param cells: Instances of (subclasses of) EntityCell.
200+
@param entities: Instances of CremeEntities (or subclass).
201+
@param user: Instance of <contrib.auth.get_user_model()>.
202+
"""
203+
cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)
204+
205+
for cell in cells:
206+
cell_groups[cell.__class__].append(cell)
207+
208+
for cell_cls, cell_group in cell_groups.items():
209+
cell_cls.populate_entities(cell_group, entities, user)
210+
211+
@staticmethod
212+
def populate_entities(cells: Iterable['EntityCell'],
213+
entities: Sequence[CremeEntity],
214+
user) -> None:
215+
"""Fill caches of CremeEntity objects with grouped SQL queries, & so
216+
avoid multiple queries when rendering the cells.
217+
The given cells MUST HAVE THE SAME TYPE (corresponding to the class
218+
the method belongs).
219+
@param cells: Instances of (subclasses of) EntityCell.
220+
@param entities: Instances of CremeEntities (or subclass).
221+
@param user: Instance of <contrib.auth.get_user_model()>.
222+
"""
183223
pass
184224

185225
# TODO: factorise render_* => like FunctionField, result that can be html, csv...

creme/creme_core/forms/header_filter.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,18 @@ def sub_widgets(self, widgets: Iterable[UniformEntityCellsWidget]):
173173
def _build_samples(self) -> List[Dict[str, str]]:
174174
user = self.user
175175
samples = []
176-
177176
cells = [*chain.from_iterable(sub_w.choices for sub_w in self._sub_widgets)]
177+
entities = [
178+
*EntityCredentials.filter(
179+
user=user, queryset=self.model.objects.order_by('-modified'),
180+
)[:2],
181+
]
182+
EntityCell.mixed_populate_entities(
183+
cells=[choice[1] for choice in cells],
184+
entities=entities, user=user,
185+
)
178186

179-
# TODO: populate entities
180-
for entity in EntityCredentials.filter(
181-
user, self.model.objects.order_by('-modified'),
182-
)[:2]:
187+
for entity in entities:
183188
dump = {}
184189

185190
for choice_id, cell in cells:

creme/creme_core/models/header_filter.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@
2020

2121
import logging
2222
# import warnings
23-
from collections import defaultdict
23+
# from collections import defaultdict
2424
from json import loads as json_load
25-
from typing import (
25+
from typing import ( # DefaultDict
2626
TYPE_CHECKING,
27-
DefaultDict,
2827
Iterable,
2928
List,
3029
Optional,
@@ -318,19 +317,21 @@ def get_edit_absolute_url(self):
318317
# )
319318
# )
320319

321-
# TODO: dispatch this job in Cells classes
322-
# => get the cells as argument, so we can pass filtered cells
323320
# TODO: way to mean QuerySet[CremeEntity] ??
324321
def populate_entities(self, entities: QuerySet, user) -> None:
325322
"""Fill caches of CremeEntity objects, related to the columns that will
326323
be displayed with this HeaderFilter.
327324
@param entities: QuerySet on CremeEntity (or subclass).
328325
@param user: Instance of get_user_model().
329326
"""
330-
cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)
331-
332-
for cell in self.cells:
333-
cell_groups[cell.__class__].append(cell)
334-
335-
for cell_cls, cell_group in cell_groups.items():
336-
cell_cls.populate_entities(cell_group, entities, user)
327+
# cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)
328+
#
329+
# for cell in self.cells:
330+
# cell_groups[cell.__class__].append(cell)
331+
#
332+
# for cell_cls, cell_group in cell_groups.items():
333+
# cell_cls.populate_entities(cell_group, entities, user)
334+
from ..core.entity_cell import EntityCell
335+
EntityCell.mixed_populate_entities(
336+
cells=self.cells, entities=entities, user=user,
337+
)

creme/creme_core/tests/core/test_entity_cell.py

+152
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@
2626
function_field_registry,
2727
)
2828
from creme.creme_core.models import (
29+
CremeEntity,
2930
CustomField,
3031
CustomFieldEnumValue,
32+
FakeCivility,
3133
FakeContact,
3234
FakeDocument,
3335
FakeFolder,
36+
FakePosition,
3437
FieldsConfig,
38+
Relation,
3539
RelationType,
3640
)
3741

@@ -597,3 +601,151 @@ def __init__(self, model, value):
597601
TestCell(model=FakeDocument, value='title'),
598602
cell1
599603
)
604+
605+
def test_mixed_populate_entities01(self):
606+
"Regular fields: no FK."
607+
user = self.create_user()
608+
609+
pos = FakePosition.objects.create(title='Pilot')
610+
create_contact = partial(FakeContact.objects.create, user=user, position_id=pos.id)
611+
contacts = [
612+
create_contact(first_name='Nagate', last_name='Tanikaze'),
613+
create_contact(first_name='Shizuka', last_name='Hoshijiro'),
614+
]
615+
616+
build = partial(EntityCellRegularField.build, model=FakeContact)
617+
cells = [build(name='last_name'), build(name='first_name')]
618+
619+
with self.assertNumQueries(0):
620+
EntityCell.mixed_populate_entities(cells=cells, entities=contacts, user=user)
621+
622+
with self.assertNumQueries(1):
623+
contacts[0].position # NOQA
624+
625+
def test_mixed_populate_entities02(self):
626+
"Regular fields: FK."
627+
user = self.create_user()
628+
629+
pos = FakePosition.objects.all()[0]
630+
civ = FakeCivility.objects.all()[0]
631+
create_contact = partial(
632+
FakeContact.objects.create, user=user, position=pos, civility=civ,
633+
)
634+
contact1 = create_contact(first_name='Nagate', last_name='Tanikaze')
635+
contact2 = create_contact(first_name='Shizuka', last_name='Hoshijiro')
636+
# NB: we refresh because the __str__() method retrieves the civility
637+
contacts = [self.refresh(contact1), self.refresh(contact2)]
638+
639+
build = partial(EntityCellRegularField.build, model=FakeContact)
640+
cells = [
641+
build(name='last_name'), build(name='first_name'),
642+
build(name='position'),
643+
build(name='civility__title'),
644+
]
645+
646+
with self.assertNumQueries(2):
647+
EntityCell.mixed_populate_entities(cells=cells, entities=contacts, user=user)
648+
649+
with self.assertNumQueries(0):
650+
contacts[0].position # NOQA
651+
contacts[1].position # NOQA
652+
contacts[0].civility # NOQA
653+
contacts[1].civility # NOQA
654+
655+
def test_mixed_populate_entities03(self):
656+
"Relationships."
657+
user = self.create_user()
658+
659+
create_rt = RelationType.create
660+
loved = create_rt(
661+
('test-subject_love', 'Is loving'),
662+
('test-object_love', 'Is loved by'),
663+
)[1]
664+
hated = create_rt(
665+
('test-subject_hate', 'Is hating'),
666+
('test-object_hate', 'Is hated by'),
667+
)[1]
668+
669+
cells = [
670+
EntityCellRegularField.build(model=FakeContact, name='last_name'),
671+
EntityCellRelation(model=FakeContact, rtype=loved),
672+
EntityCellRelation(model=FakeContact, rtype=hated),
673+
]
674+
675+
create_contact = partial(FakeContact.objects.create, user=user)
676+
nagate = create_contact(first_name='Nagate', last_name='Tanikaze')
677+
shizuka = create_contact(first_name='Shizuka', last_name='Hoshijiro')
678+
izana = create_contact(first_name='Izana', last_name='Shinatose')
679+
norio = create_contact(first_name='Norio', last_name='Kunato')
680+
681+
create_rel = partial(Relation.objects.create, user=user)
682+
create_rel(subject_entity=nagate, type=loved, object_entity=izana)
683+
create_rel(subject_entity=nagate, type=hated, object_entity=norio)
684+
create_rel(subject_entity=shizuka, type=loved, object_entity=norio)
685+
686+
# NB: sometimes a query to get this CT is performed when the Relations
687+
# are retrieved. So we force the cache to be filled has he should be
688+
ContentType.objects.get_for_model(CremeEntity)
689+
690+
with self.assertNumQueries(2):
691+
EntityCell.mixed_populate_entities(cells, [nagate, shizuka], user)
692+
693+
with self.assertNumQueries(0):
694+
r1 = nagate.get_relations(loved.id, real_obj_entities=True)
695+
r2 = nagate.get_relations(hated.id, real_obj_entities=True)
696+
r3 = shizuka.get_relations(loved.id, real_obj_entities=True)
697+
r4 = shizuka.get_relations(hated.id, real_obj_entities=True)
698+
699+
with self.assertNumQueries(0):
700+
objs1 = [r.object_entity.get_real_entity() for r in r1]
701+
objs2 = [r.object_entity.get_real_entity() for r in r2]
702+
objs3 = [r.object_entity.get_real_entity() for r in r3]
703+
objs4 = [r.object_entity.get_real_entity() for r in r4]
704+
705+
self.assertListEqual([izana], objs1)
706+
self.assertListEqual([norio], objs2)
707+
self.assertListEqual([norio], objs3)
708+
self.assertListEqual([], objs4)
709+
710+
def test_mixed_populate_entities04(self):
711+
"Mixed types."
712+
user = self.create_user()
713+
714+
pos = FakePosition.objects.all()[0]
715+
create_contact = partial(FakeContact.objects.create, user=user)
716+
contacts = [
717+
create_contact(first_name='Nagate', last_name='Tanikaze', position=pos),
718+
create_contact(first_name='Shizuka', last_name='Hoshijiro'),
719+
create_contact(first_name='Izana', last_name='Shinatose'),
720+
]
721+
722+
loved = RelationType.create(
723+
('test-subject_love', 'Is loving'),
724+
('test-object_love', 'Is loved by'),
725+
)[1]
726+
Relation.objects.create(
727+
user=user, subject_entity=contacts[0], type=loved, object_entity=contacts[2],
728+
)
729+
730+
build_rfield = partial(EntityCellRegularField.build, model=FakeContact)
731+
cells = [
732+
build_rfield(name='last_name'),
733+
build_rfield(name='position'),
734+
EntityCellRelation(model=FakeContact, rtype=loved),
735+
]
736+
737+
# NB: sometimes a query to get this CT is performed when the Relations
738+
# are retrieved. So we force the cache to be filled has he should be
739+
ContentType.objects.get_for_model(CremeEntity)
740+
741+
# Drop caches
742+
contacts = [self.refresh(c) for c in contacts]
743+
744+
with self.assertNumQueries(3):
745+
EntityCell.mixed_populate_entities(cells, contacts, user)
746+
747+
with self.assertNumQueries(0):
748+
contacts[0].position # NOQA
749+
750+
with self.assertNumQueries(0):
751+
contacts[0].get_relations(loved.id, real_obj_entities=True)

0 commit comments

Comments
 (0)