Skip to content

Commit

Permalink
[ci skip] Add a method 'EntityCell.mixed_populate_entities()'.
Browse files Browse the repository at this point in the history
  • Loading branch information
genglert committed Feb 18, 2021
1 parent e4aed51 commit 48951af
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 103 deletions.
44 changes: 42 additions & 2 deletions creme/creme_core/core/entity_cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@
################################################################################

import logging
from typing import Dict, Iterable, List, Optional, Tuple, Type # Callable
from collections import defaultdict
from typing import ( # Callable
DefaultDict,
Dict,
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
)

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

@staticmethod
def populate_entities(cells, entities, user):
def mixed_populate_entities(cells: Iterable['EntityCell'],
entities: Sequence[CremeEntity],
user) -> None:
"""Fill caches of CremeEntity objects with grouped SQL queries, & so
avoid multiple queries when rendering the cells.
The given cells are grouped by types, and then the method
'populate_entities()' of each used type is called.
@param cells: Instances of (subclasses of) EntityCell.
@param entities: Instances of CremeEntities (or subclass).
@param user: Instance of <contrib.auth.get_user_model()>.
"""
cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)

for cell in cells:
cell_groups[cell.__class__].append(cell)

for cell_cls, cell_group in cell_groups.items():
cell_cls.populate_entities(cell_group, entities, user)

@staticmethod
def populate_entities(cells: Iterable['EntityCell'],
entities: Sequence[CremeEntity],
user) -> None:
"""Fill caches of CremeEntity objects with grouped SQL queries, & so
avoid multiple queries when rendering the cells.
The given cells MUST HAVE THE SAME TYPE (corresponding to the class
the method belongs).
@param cells: Instances of (subclasses of) EntityCell.
@param entities: Instances of CremeEntities (or subclass).
@param user: Instance of <contrib.auth.get_user_model()>.
"""
pass

# TODO: factorise render_* => like FunctionField, result that can be html, csv...
Expand Down
15 changes: 10 additions & 5 deletions creme/creme_core/forms/header_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,18 @@ def sub_widgets(self, widgets: Iterable[UniformEntityCellsWidget]):
def _build_samples(self) -> List[Dict[str, str]]:
user = self.user
samples = []

cells = [*chain.from_iterable(sub_w.choices for sub_w in self._sub_widgets)]
entities = [
*EntityCredentials.filter(
user=user, queryset=self.model.objects.order_by('-modified'),
)[:2],
]
EntityCell.mixed_populate_entities(
cells=[choice[1] for choice in cells],
entities=entities, user=user,
)

# TODO: populate entities
for entity in EntityCredentials.filter(
user, self.model.objects.order_by('-modified'),
)[:2]:
for entity in entities:
dump = {}

for choice_id, cell in cells:
Expand Down
25 changes: 13 additions & 12 deletions creme/creme_core/models/header_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@

import logging
# import warnings
from collections import defaultdict
# from collections import defaultdict
from json import loads as json_load
from typing import (
from typing import ( # DefaultDict
TYPE_CHECKING,
DefaultDict,
Iterable,
List,
Optional,
Expand Down Expand Up @@ -318,19 +317,21 @@ def get_edit_absolute_url(self):
# )
# )

# TODO: dispatch this job in Cells classes
# => get the cells as argument, so we can pass filtered cells
# TODO: way to mean QuerySet[CremeEntity] ??
def populate_entities(self, entities: QuerySet, user) -> None:
"""Fill caches of CremeEntity objects, related to the columns that will
be displayed with this HeaderFilter.
@param entities: QuerySet on CremeEntity (or subclass).
@param user: Instance of get_user_model().
"""
cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)

for cell in self.cells:
cell_groups[cell.__class__].append(cell)

for cell_cls, cell_group in cell_groups.items():
cell_cls.populate_entities(cell_group, entities, user)
# cell_groups: DefaultDict[Type['EntityCell'], List['EntityCell']] = defaultdict(list)
#
# for cell in self.cells:
# cell_groups[cell.__class__].append(cell)
#
# for cell_cls, cell_group in cell_groups.items():
# cell_cls.populate_entities(cell_group, entities, user)
from ..core.entity_cell import EntityCell
EntityCell.mixed_populate_entities(
cells=self.cells, entities=entities, user=user,
)
152 changes: 152 additions & 0 deletions creme/creme_core/tests/core/test_entity_cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@
function_field_registry,
)
from creme.creme_core.models import (
CremeEntity,
CustomField,
CustomFieldEnumValue,
FakeCivility,
FakeContact,
FakeDocument,
FakeFolder,
FakePosition,
FieldsConfig,
Relation,
RelationType,
)

Expand Down Expand Up @@ -597,3 +601,151 @@ def __init__(self, model, value):
TestCell(model=FakeDocument, value='title'),
cell1
)

def test_mixed_populate_entities01(self):
"Regular fields: no FK."
user = self.create_user()

pos = FakePosition.objects.create(title='Pilot')
create_contact = partial(FakeContact.objects.create, user=user, position_id=pos.id)
contacts = [
create_contact(first_name='Nagate', last_name='Tanikaze'),
create_contact(first_name='Shizuka', last_name='Hoshijiro'),
]

build = partial(EntityCellRegularField.build, model=FakeContact)
cells = [build(name='last_name'), build(name='first_name')]

with self.assertNumQueries(0):
EntityCell.mixed_populate_entities(cells=cells, entities=contacts, user=user)

with self.assertNumQueries(1):
contacts[0].position # NOQA

def test_mixed_populate_entities02(self):
"Regular fields: FK."
user = self.create_user()

pos = FakePosition.objects.all()[0]
civ = FakeCivility.objects.all()[0]
create_contact = partial(
FakeContact.objects.create, user=user, position=pos, civility=civ,
)
contact1 = create_contact(first_name='Nagate', last_name='Tanikaze')
contact2 = create_contact(first_name='Shizuka', last_name='Hoshijiro')
# NB: we refresh because the __str__() method retrieves the civility
contacts = [self.refresh(contact1), self.refresh(contact2)]

build = partial(EntityCellRegularField.build, model=FakeContact)
cells = [
build(name='last_name'), build(name='first_name'),
build(name='position'),
build(name='civility__title'),
]

with self.assertNumQueries(2):
EntityCell.mixed_populate_entities(cells=cells, entities=contacts, user=user)

with self.assertNumQueries(0):
contacts[0].position # NOQA
contacts[1].position # NOQA
contacts[0].civility # NOQA
contacts[1].civility # NOQA

def test_mixed_populate_entities03(self):
"Relationships."
user = self.create_user()

create_rt = RelationType.create
loved = create_rt(
('test-subject_love', 'Is loving'),
('test-object_love', 'Is loved by'),
)[1]
hated = create_rt(
('test-subject_hate', 'Is hating'),
('test-object_hate', 'Is hated by'),
)[1]

cells = [
EntityCellRegularField.build(model=FakeContact, name='last_name'),
EntityCellRelation(model=FakeContact, rtype=loved),
EntityCellRelation(model=FakeContact, rtype=hated),
]

create_contact = partial(FakeContact.objects.create, user=user)
nagate = create_contact(first_name='Nagate', last_name='Tanikaze')
shizuka = create_contact(first_name='Shizuka', last_name='Hoshijiro')
izana = create_contact(first_name='Izana', last_name='Shinatose')
norio = create_contact(first_name='Norio', last_name='Kunato')

create_rel = partial(Relation.objects.create, user=user)
create_rel(subject_entity=nagate, type=loved, object_entity=izana)
create_rel(subject_entity=nagate, type=hated, object_entity=norio)
create_rel(subject_entity=shizuka, type=loved, object_entity=norio)

# NB: sometimes a query to get this CT is performed when the Relations
# are retrieved. So we force the cache to be filled has he should be
ContentType.objects.get_for_model(CremeEntity)

with self.assertNumQueries(2):
EntityCell.mixed_populate_entities(cells, [nagate, shizuka], user)

with self.assertNumQueries(0):
r1 = nagate.get_relations(loved.id, real_obj_entities=True)
r2 = nagate.get_relations(hated.id, real_obj_entities=True)
r3 = shizuka.get_relations(loved.id, real_obj_entities=True)
r4 = shizuka.get_relations(hated.id, real_obj_entities=True)

with self.assertNumQueries(0):
objs1 = [r.object_entity.get_real_entity() for r in r1]
objs2 = [r.object_entity.get_real_entity() for r in r2]
objs3 = [r.object_entity.get_real_entity() for r in r3]
objs4 = [r.object_entity.get_real_entity() for r in r4]

self.assertListEqual([izana], objs1)
self.assertListEqual([norio], objs2)
self.assertListEqual([norio], objs3)
self.assertListEqual([], objs4)

def test_mixed_populate_entities04(self):
"Mixed types."
user = self.create_user()

pos = FakePosition.objects.all()[0]
create_contact = partial(FakeContact.objects.create, user=user)
contacts = [
create_contact(first_name='Nagate', last_name='Tanikaze', position=pos),
create_contact(first_name='Shizuka', last_name='Hoshijiro'),
create_contact(first_name='Izana', last_name='Shinatose'),
]

loved = RelationType.create(
('test-subject_love', 'Is loving'),
('test-object_love', 'Is loved by'),
)[1]
Relation.objects.create(
user=user, subject_entity=contacts[0], type=loved, object_entity=contacts[2],
)

build_rfield = partial(EntityCellRegularField.build, model=FakeContact)
cells = [
build_rfield(name='last_name'),
build_rfield(name='position'),
EntityCellRelation(model=FakeContact, rtype=loved),
]

# NB: sometimes a query to get this CT is performed when the Relations
# are retrieved. So we force the cache to be filled has he should be
ContentType.objects.get_for_model(CremeEntity)

# Drop caches
contacts = [self.refresh(c) for c in contacts]

with self.assertNumQueries(3):
EntityCell.mixed_populate_entities(cells, contacts, user)

with self.assertNumQueries(0):
contacts[0].position # NOQA

with self.assertNumQueries(0):
contacts[0].get_relations(loved.id, real_obj_entities=True)
Loading

0 comments on commit 48951af

Please sign in to comment.