Add proximity sorting of parties

issue5853
review28731002
This commit is contained in:
Cédric Krier committed 2020-03-07 00:27:13 +01:00
1 parent 853ce6da8a
commit bf9135f3eb
6 files changed
+261 -1

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
* Add proximity sorting of parties
Version 5.4.0 - 2019-11-04
* Bug fixes (see mercurial logs for details)
+1
View File
@@ -10,4 +10,5 @@ def register():
party.Relation,
party.RelationAll,
party.Party,
party.ContactMechanism,
module='party_relationship', type_='model')
+16
View File
@@ -7,3 +7,19 @@ between parties.
Each relation is defined by a relation type. A reverse relation type can be
defined, so when creating a relation of a type, the reverse relation will be
automatically created.
It is possible to order parties by how closely related they are to a defined
party. The distance is calculated based on the number of steps it takes to get
from the defined party to another. By default all the different types of
relationship are considered, but this can be limited by adding
``relation_usages`` to the context.
Configuration
*************
The party_relationship module use the section `party_relationship` to retrieve
some parameters:
- `depth`: The maximum number of steps to consider when calculating the
distance between parties.
The default value is `7`.
+127 -1
View File
@@ -1,11 +1,19 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from sql import Union, As, Column, Null
import json
from functools import partial
from sql import Union, As, Column, Null, Literal, With, Select
from sql.aggregate import Min
from trytond.config import config
from trytond.pool import Pool, PoolMeta
from trytond.model import ModelSQL, ModelView, fields
from trytond.transaction import Transaction
dumps = partial(json.dumps, separators=(',', ':'), sort_keys=True)
default_depth = config.getint('party_relationship', 'depth', default=7)
class RelationType(ModelSQL, ModelView):
'Relation Type'
@@ -15,6 +23,18 @@ class RelationType(ModelSQL, ModelView):
help="The main identifier of the relation type.")
reverse = fields.Many2One('party.relation.type', 'Reverse Relation',
help="Create automatically the reverse relation.")
usages = fields.MultiSelection([], "Usages")
@classmethod
def view_attributes(cls):
attributes = super().view_attributes()
if not cls.usages.selection:
attributes.extend([
('//separator[@name="usages"]',
'states', {'invisible': True}),
('//field[@name="usages"]', 'invisible', 1),
])
return attributes
class Relation(ModelSQL):
@@ -205,3 +225,109 @@ class Party(metaclass=PoolMeta):
__name__ = 'party.party'
relations = fields.One2Many('party.relation.all', 'from_', 'Relations')
distance = fields.Function(fields.Integer('Distance'), 'get_distance')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('distance', 'ASC NULLS LAST'))
@classmethod
def _distance_query(cls, usages=None, party=None, depth=None):
pool = Pool()
RelationAll = pool.get('party.relation.all')
RelationType = pool.get('party.relation.type')
transaction = Transaction()
context = transaction.context
database = transaction.database
if usages is None:
usages = context.get('relation_usages', [])
if party is None:
party = context.get('related_party')
if depth is None:
depth = context.get('depth', default_depth)
if not party:
return
all_relations = RelationAll.__table__()
if usages:
relation_type = RelationType.__table__()
try:
usages_clause = database.json_any_keys_exist(
relation_type.usages, list(usages))
except NotImplementedError:
usages_clause = Literal(False)
for usage in usages:
usages_clause |= relation_type.usages.like(
'%' + dumps(usage) + '%')
relations = (all_relations
.join(relation_type,
condition=all_relations.type == relation_type.id)
.select(
all_relations.from_, all_relations.to,
where=usages_clause))
else:
relations = all_relations
distance = With('from_', 'to', 'distance', recursive=True)
distance.query = relations.select(
Column(relations, 'from_'),
relations.to,
Literal(1).as_('distance'),
where=Column(relations, 'from_') == party)
distance.query |= (distance
.join(relations,
condition=distance.to == Column(relations, 'from_'))
.select(
distance.from_,
relations.to,
(distance.distance + Literal(1)).as_('distance'),
where=(relations.to != party)
& (distance.distance < depth)))
distance.query.all_ = True
return (distance
.select(
distance.to, Min(distance.distance).as_('distance'),
group_by=[distance.to], with_=[distance])
| Select([Literal(party).as_('to'), Literal(0).as_('distance')]))
@classmethod
def order_distance(cls, tables):
party, _ = tables[None]
key = 'distance'
if key not in tables:
query = cls._distance_query()
if not query:
return []
join = party.join(query, type_='LEFT',
condition=query.to == party.id)
tables[key] = {
None: (join.right, join.condition),
}
else:
query, _ = tables[key][None]
return [query.distance]
@classmethod
def get_distance(cls, parties, name):
distances = {p.id: None for p in parties}
query = cls._distance_query()
if query:
cursor = Transaction().connection.cursor()
cursor.execute(*query)
distances.update(cursor)
return distances
class ContactMechanism(metaclass=PoolMeta):
__name__ = 'party.contact_mechanism'
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('party.distance', 'ASC NULLS LAST'))
+113
View File
@@ -4,6 +4,7 @@ import unittest
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.tests.test_tryton import suite as tryton_suite
from trytond.pool import Pool
from trytond.transaction import Transaction
class TestCase(ModuleTestCase):
@@ -173,6 +174,118 @@ class TestCase(ModuleTestCase):
RelationAll.delete([reverse_relation])
self.assertEqual(RelationAll.search([]), [])
@with_transaction()
def test_party_distance(self):
"Test party distance"
pool = Pool()
Party = pool.get('party.party')
RelationType = pool.get('party.relation.type')
Relation = pool.get('party.relation')
usages = RelationType.usages.selection
self.addCleanup(setattr, RelationType.usages, 'selection', usages)
RelationType.usages.selection = usages + [('test', "Test")]
relation, reverse = RelationType.create([{
'name': 'Relation',
}, {
'name': 'Reverse',
}])
relation.reverse = reverse
relation.save()
reverse.reverse = relation
reverse.save()
A, B, C, D = Party.create([{
'name': 'A',
}, {
'name': 'B',
}, {
'name': 'C',
}, {
'name': 'D'
}])
Relation.create([{
'from_': A.id,
'to': C.id,
'type': relation.id,
}, {
'from_': C.id,
'to': D.id,
'type': relation.id,
}])
parties = Party.search([])
self.assertEqual([p.distance for p in parties], [None] * 4)
with Transaction().set_context(related_party=A.id):
parties = Party.search([])
self.assertEqual(
[(p.name, p.distance) for p in parties],
[('A', 0), ('C', 1), ('D', 2), ('B', None)])
another_relation, = RelationType.create([{
'name': 'Another Relation',
'usages': ['test'],
}])
Relation.create([{
'from_': A.id,
'to': B.id,
'type': another_relation.id,
}])
with Transaction().set_context(
related_party=A.id, relation_usages=['test']):
parties = Party.search([])
self.assertEqual(
[(p.name, p.distance) for p in parties],
[('A', 0), ('B', 1), ('C', None), ('D', None)])
with Transaction().set_context(related_party=A.id):
parties = Party.search([])
self.assertEqual(
[(p.name, p.distance) for p in parties],
[('A', 0), ('B', 1), ('C', 1), ('D', 2)])
@with_transaction()
def test_contact_mechanism_distance(self):
"Test relation distance"
pool = Pool()
Party = pool.get('party.party')
ContactMechanism = pool.get('party.contact_mechanism')
RelationType = pool.get('party.relation.type')
Relation = pool.get('party.relation')
relation, = RelationType.create([{
'name': 'Relation',
}])
A, B, C = Party.create([{
'name': "A",
'contact_mechanisms': [
('create', [{'value': "A", 'type': 'other'}])],
}, {
'name': "B",
'contact_mechanisms': [
('create', [{'value': "B", 'type': 'other'}])],
}, {
'name': "C",
'contact_mechanisms': [
('create', [{'value': "C", 'type': 'other'}])],
}])
Relation.create([{
'from_': A.id,
'to': C.id,
'type': relation.id,
}])
with Transaction().set_context(related_party=A.id):
contact_mechanisms = ContactMechanism.search([])
self.assertEqual(
[c.value for c in contact_mechanisms],
['A', 'C', 'B'])
def suite():
suite = tryton_suite()
+2
View File
@@ -6,5 +6,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<label name="reverse"/>
<field name="reverse"/>
<separator name="usages" colspan="4"/>
<field name="usages" colspan="4"/>
</form>