Add proximity sorting of parties
issue5853 review28731002
This commit is contained in:
1 parent
853ce6da8a
commit
bf9135f3eb
6 files changed
+261
-1
No files matched your search
@@ -1,3 +1,5 @@
|
||||
* Add proximity sorting of parties
|
||||
|
||||
Version 5.4.0 - 2019-11-04
|
||||
* Bug fixes (see mercurial logs for details)
|
||||
|
||||
|
||||
@@ -10,4 +10,5 @@ def register():
|
||||
party.Relation,
|
||||
party.RelationAll,
|
||||
party.Party,
|
||||
party.ContactMechanism,
|
||||
module='party_relationship', type_='model')
|
||||
@@ -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`.
|
||||
@@ -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'))
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user