changeset d1109e268342 in modules/party_relationship:default
details: 
https://hg.tryton.org/modules/party_relationship?cmd=changeset;node=d1109e268342
description:
        Add proximity sorting of parties

        issue5853
        review28731002
diffstat:

 CHANGELOG                        |    2 +
 __init__.py                      |    1 +
 doc/index.rst                    |   16 ++++
 party.py                         |  128 ++++++++++++++++++++++++++++++++++++++-
 tests/test_party_relationship.py |  113 ++++++++++++++++++++++++++++++++++
 view/relation_type_form.xml      |    2 +
 6 files changed, 261 insertions(+), 1 deletions(-)

diffs (335 lines):

diff -r 9ada0105c9ae -r d1109e268342 CHANGELOG
--- a/CHANGELOG Sun Mar 01 16:12:39 2020 +0100
+++ b/CHANGELOG Sat Mar 07 00:27:13 2020 +0100
@@ -1,3 +1,5 @@
+* Add proximity sorting of parties
+
 Version 5.4.0 - 2019-11-04
 * Bug fixes (see mercurial logs for details)
 
diff -r 9ada0105c9ae -r d1109e268342 __init__.py
--- a/__init__.py       Sun Mar 01 16:12:39 2020 +0100
+++ b/__init__.py       Sat Mar 07 00:27:13 2020 +0100
@@ -10,4 +10,5 @@
         party.Relation,
         party.RelationAll,
         party.Party,
+        party.ContactMechanism,
         module='party_relationship', type_='model')
diff -r 9ada0105c9ae -r d1109e268342 doc/index.rst
--- a/doc/index.rst     Sun Mar 01 16:12:39 2020 +0100
+++ b/doc/index.rst     Sat Mar 07 00:27:13 2020 +0100
@@ -7,3 +7,19 @@
 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`.
diff -r 9ada0105c9ae -r d1109e268342 party.py
--- a/party.py  Sun Mar 01 16:12:39 2020 +0100
+++ b/party.py  Sat Mar 07 00:27:13 2020 +0100
@@ -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 @@
         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 @@
     __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'))
diff -r 9ada0105c9ae -r d1109e268342 tests/test_party_relationship.py
--- a/tests/test_party_relationship.py  Sun Mar 01 16:12:39 2020 +0100
+++ b/tests/test_party_relationship.py  Sat Mar 07 00:27:13 2020 +0100
@@ -4,6 +4,7 @@
 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 @@
         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()
diff -r 9ada0105c9ae -r d1109e268342 view/relation_type_form.xml
--- a/view/relation_type_form.xml       Sun Mar 01 16:12:39 2020 +0100
+++ b/view/relation_type_form.xml       Sat Mar 07 00:27:13 2020 +0100
@@ -6,5 +6,7 @@
     <field name="name"/>
     <label name="reverse"/>
     <field name="reverse"/>
+    <separator name="usages" colspan="4"/>
+    <field name="usages" colspan="4"/>
 </form>
 

Reply via email to