From d61c48f298109bf5d94342eca8564c67f0b89ee7 Mon Sep 17 00:00:00 2001 From: Xavier-Do Date: Thu, 10 Oct 2019 14:59:21 +0000 Subject: [PATCH] [FIX] core: remove useless graph class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes odoo/odoo#38967 Signed-off-by: Xavier Dollé (xdo) --- odoo/addons/base/models/ir_ui_view.py | 75 +-- odoo/tools/__init__.py | 1 - odoo/tools/graph.py | 755 -------------------------- 3 files changed, 1 insertion(+), 830 deletions(-) delete mode 100755 odoo/tools/graph.py diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 2eabcdc69ec..613ed6e0be1 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -23,7 +23,7 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError from odoo.http import request from odoo.modules.module import get_resource_from_path, get_resource_path -from odoo.tools import config, graph, ConstantMapping, pycompat, apply_inheritance_specs, locate_node +from odoo.tools import config, ConstantMapping, pycompat, apply_inheritance_specs, locate_node from odoo.tools.convert import _fix_multiple_roots from odoo.tools.json import scriptsafe as json_scriptsafe from odoo.tools.safe_eval import safe_eval @@ -1159,79 +1159,6 @@ actual arch. """ Open a view for editing the translations of field 'arch_db'. """ return self.env['ir.translation'].translate_fields('ir.ui.view', self.id, 'arch_db') - @api.model - def graph_get(self, id, model, node_obj, conn_obj, src_node, des_node, label, scale): - def rec_name(rec): - return (rec.name if 'name' in rec else - rec.x_name if 'x_name' in rec else - None) - - nodes = [] - nodes_name = [] - transitions = [] - start = [] - tres = {} - labels = {} - no_ancester = [] - blank_nodes = [] - - Model = self.env[model] - Node = self.env[node_obj] - - for model_key, model_value in Model._fields.items(): - if model_value.type == 'one2many': - if model_value.comodel_name == node_obj: - _Node_Field = model_key - _Model_Field = model_value.inverse_name - for node_key, node_value in Node._fields.items(): - if node_value.type == 'one2many': - if node_value.comodel_name == conn_obj: - # _Source_Field = "Incoming Arrows" (connected via des_node) - if node_value.inverse_name == des_node: - _Source_Field = node_key - # _Destination_Field = "Outgoing Arrows" (connected via src_node) - if node_value.inverse_name == src_node: - _Destination_Field = node_key - - record = Model.browse(id) - for line in record[_Node_Field]: - if line[_Source_Field] or line[_Destination_Field]: - nodes_name.append((line.id, rec_name(line))) - nodes.append(line.id) - else: - blank_nodes.append({'id': line.id, 'name': rec_name(line)}) - - if 'flow_start' in line and line.flow_start: - start.append(line.id) - elif not line[_Source_Field]: - no_ancester.append(line.id) - - for t in line[_Destination_Field]: - transitions.append((line.id, t[des_node].id)) - tres[str(t['id'])] = (line.id, t[des_node].id) - label_string = "" - if label: - for lbl in safe_eval(label): - if tools.ustr(lbl) in t and tools.ustr(t[lbl]) == 'False': - label_string += ' ' - else: - label_string = label_string + " " + tools.ustr(t[lbl]) - labels[str(t['id'])] = (line.id, label_string) - - g = graph(nodes, transitions, no_ancester) - g.process(start) - g.scale(*scale) - result = g.result_get() - results = {} - for node_id, node_name in nodes_name: - results[str(node_id)] = result[node_id] - results[str(node_id)]['name'] = node_name - return {'nodes': results, - 'transitions': tres, - 'label': labels, - 'blank_nodes': blank_nodes, - 'node_parent_field': _Model_Field} - @api.model def _validate_custom_views(self, model): """Validate architecture of custom views (= without xml id) for a given model. diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py index 4ac0af8ddfe..d2666bd1cbf 100644 --- a/odoo/tools/__init__.py +++ b/odoo/tools/__init__.py @@ -8,7 +8,6 @@ from . import pdf from .config import config from .misc import * from .translate import * -from .graph import graph from .image import * from .sql import * from .float_utils import * diff --git a/odoo/tools/graph.py b/odoo/tools/graph.py deleted file mode 100755 index 894b7f3bd2b..00000000000 --- a/odoo/tools/graph.py +++ /dev/null @@ -1,755 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import operator -import math - - -class graph(object): - def __init__(self, nodes, transitions, no_ancester=None): - """Initialize graph's object - - @param nodes list of ids of nodes in the graph - @param transitions list of edges in the graph in the form (source_node, destination_node) - @param no_ancester list of nodes with no incoming edges - """ - - self.nodes = nodes or [] - self.edges = transitions or [] - self.no_ancester = no_ancester or {} - trans = {} - - for t in transitions: - trans.setdefault(t[0], []) - trans[t[0]].append(t[1]) - self.transitions = trans - self.result = {} - - - def init_rank(self): - """Computes rank of the nodes of the graph by finding initial feasible tree - """ - self.edge_wt = {} - for link in self.links: - self.edge_wt[link] = self.result[link[1]]['x'] - self.result[link[0]]['x'] - - tot_node = len(self.partial_order) - #do until all the nodes in the component are searched - while self.tight_tree() self.edge_wt[edge]-1: - slack = self.edge_wt[edge]-1 - new_edge = edge - - if new_edge[0] not in self.reachable_nodes: - delta = -(self.edge_wt[new_edge]-1) - else: - delta = self.edge_wt[new_edge]-1 - - for node in self.result: - if node in self.reachable_nodes: - self.result[node]['x'] += delta - - for edge in self.edge_wt: - self.edge_wt[edge] = self.result[edge[1]]['x'] - self.result[edge[0]]['x'] - - self.init_cutvalues() - - - def tight_tree(self): - self.reachable_nodes = [] - self.tree_edges = [] - self.reachable_node(self.start) - return len(self.reachable_nodes) - - - def reachable_node(self, node): - """Find the nodes of the graph which are only 1 rank apart from each other - """ - - if node not in self.reachable_nodes: - self.reachable_nodes.append(node) - for edge in self.edge_wt: - if edge[0]==node: - if self.edge_wt[edge]==1: - self.tree_edges.append(edge) - if edge[1] not in self.reachable_nodes: - self.reachable_nodes.append(edge[1]) - self.reachable_node(edge[1]) - - - def init_cutvalues(self): - """Initialize cut values of edges of the feasible tree. - Edges with negative cut-values are removed from the tree to optimize rank assignment - """ - self.cut_edges = {} - self.head_nodes = [] - i=0 - - for edge in self.tree_edges: - self.head_nodes = [] - rest_edges = [] - rest_edges += self.tree_edges - del rest_edges[i] - self.head_component(self.start, rest_edges) - i+=1 - positive = 0 - negative = 0 - for source_node in self.transitions: - if source_node in self.head_nodes: - for dest_node in self.transitions[source_node]: - if dest_node not in self.head_nodes: - negative+=1 - else: - for dest_node in self.transitions[source_node]: - if dest_node in self.head_nodes: - positive+=1 - - self.cut_edges[edge] = positive - negative - - - def head_component(self, node, rest_edges): - """Find nodes which are reachable from the starting node, after removing an edge - """ - if node not in self.head_nodes: - self.head_nodes.append(node) - - for edge in rest_edges: - if edge[0]==node: - self.head_component(edge[1],rest_edges) - - - def process_ranking(self, node, level=0): - """Computes initial feasible ranking after making graph acyclic with depth-first search - """ - - if node not in self.result: - self.result[node] = {'y': None, 'x':level, 'mark':0} - else: - if level > self.result[node]['x']: - self.result[node]['x'] = level - - if self.result[node]['mark']==0: - self.result[node]['mark'] = 1 - for sec_end in self.transitions.get(node, []): - self.process_ranking(sec_end, level+1) - - - def make_acyclic(self, parent, node, level, tree): - """Computes Partial-order of the nodes with depth-first search - """ - - if node not in self.partial_order: - self.partial_order[node] = {'level':level, 'mark':0} - if parent: - tree.append((parent, node)) - - if self.partial_order[node]['mark']==0: - self.partial_order[node]['mark'] = 1 - for sec_end in self.transitions.get(node, []): - self.links.append((node, sec_end)) - self.make_acyclic(node, sec_end, level+1, tree) - - return tree - - - def rev_edges(self, tree): - """reverse the direction of the edges whose source-node-partail_order> destination-node-partail_order - to make the graph acyclic - """ - Is_Cyclic = False - i=0 - for link in self.links: - src = link[0] - des = link[1] - edge_len = self.partial_order[des]['level'] - self.partial_order[src]['level'] - if edge_len < 0: - del self.links[i] - self.links.insert(i, (des, src)) - self.transitions[src].remove(des) - self.transitions.setdefault(des, []).append(src) - Is_Cyclic = True - elif math.fabs(edge_len) > 1: - Is_Cyclic = True - i += 1 - - return Is_Cyclic - - def exchange(self, e, f): - """Exchange edges to make feasible-tree optimized - :param e: edge with negative cut-value - :param f: new edge with minimum slack-value - """ - del self.tree_edges[self.tree_edges.index(e)] - self.tree_edges.append(f) - self.init_cutvalues() - - - def enter_edge(self, edge): - """Finds a new_edge with minimum slack value to replace an edge with negative cut-value - - @param edge edge with negative cut-value - """ - - self.head_nodes = [] - rest_edges = [] - rest_edges += self.tree_edges - del rest_edges[rest_edges.index(edge)] - self.head_component(self.start, rest_edges) - - if edge[1] in self.head_nodes: - l = [] - for node in self.result: - if node not in self.head_nodes: - l.append(node) - self.head_nodes = l - - slack = 100 - new_edge = edge - for source_node in self.transitions: - if source_node in self.head_nodes: - for dest_node in self.transitions[source_node]: - if dest_node not in self.head_nodes: - if slack>(self.edge_wt[edge]-1): - slack = self.edge_wt[edge]-1 - new_edge = (source_node, dest_node) - - return new_edge - - - def leave_edge(self): - """Returns the edge with negative cut_value(if exists) - """ - if self.critical_edges: - for edge in self.critical_edges: - self.cut_edges[edge] = 0 - - for edge in self.cut_edges: - if self.cut_edges[edge]<0: - return edge - - return None - - - def finalize_rank(self, node, level): - self.result[node]['x'] = level - for destination in self.optimal_edges.get(node, []): - self.finalize_rank(destination, level+1) - - - def normalize(self): - """The ranks are normalized by setting the least rank to zero. - """ - - least_rank = min(x['x'] for x in self.result.values()) - - if least_rank!=0: - for node in self.result: - self.result[node]['x']-=least_rank - - - def make_chain(self): - """Edges between nodes more than one rank apart are replaced by chains of unit - length edges between temporary nodes. - """ - - for edge in self.edge_wt: - if self.edge_wt[edge]>1: - self.transitions[edge[0]].remove(edge[1]) - start = self.result[edge[0]]['x'] - end = self.result[edge[1]]['x'] - - for rank in range(start+1, end): - if not self.result.get((rank, 'temp'), False): - self.result[(rank, 'temp')] = {'y': None, 'x': rank, 'mark': 0} - - for rank in range(start, end): - if start==rank: - self.transitions[edge[0]].append((rank+1, 'temp')) - elif rank==end-1: - self.transitions.setdefault((rank, 'temp'), []).append(edge[1]) - else: - self.transitions.setdefault((rank, 'temp'), []).append((rank+1, 'temp')) - - - def init_order(self, node, level): - """Initialize orders the nodes in each rank with depth-first search - """ - if not self.result[node]['y']: - self.result[node]['y'] = self.order[level] - self.order[level] += 1 - - for sec_end in self.transitions.get(node, []): - if node!=sec_end: - self.init_order(sec_end, self.result[sec_end]['x']) - - - def order_heuristic(self): - for i in range(12): - self.wmedian() - - - def wmedian(self): - """Applies median heuristic to find optimized order of the nodes with in their ranks - """ - for level in self.levels: - - node_median = [] - nodes = self.levels[level] - for node in nodes: - node_median.append((node, self.median_value(node, level-1))) - - sort_list = sorted(node_median, key=operator.itemgetter(1)) - - new_list = [tuple[0] for tuple in sort_list] - - self.levels[level] = new_list - order = 0 - for node in nodes: - self.result[node]['y'] = order - order +=1 - - - def median_value(self, node, adj_rank): - """Returns median value of a vertex , defined as the median position of the adjacent vertices - - @param node node to process - @param adj_rank rank 1 less than the node's rank - """ - adj_nodes = self.adj_position(node, adj_rank) - l = len(adj_nodes) - m = l // 2 - - if l==0: - return -1.0 - elif l%2 == 1: - return adj_nodes[m]#median of the middle element - elif l==2: - return (adj_nodes[0]+adj_nodes[1])/2 - else: - left = adj_nodes[m-1] - adj_nodes[0] - right = adj_nodes[l-1] - adj_nodes[m] - return ((adj_nodes[m-1]*right) + (adj_nodes[m]*left))/(left+right) - - - def adj_position(self, node, adj_rank): - """Returns list of the present positions of the nodes adjacent to node in the given adjacent rank. - - @param node node to process - @param adj_rank rank 1 less than the node's rank - """ - - pre_level_nodes = self.levels.get(adj_rank, []) - adj_nodes = [] - - if pre_level_nodes: - for src in pre_level_nodes: - if self.transitions.get(src) and node in self.transitions[src]: - adj_nodes.append(self.result[src]['y']) - - return adj_nodes - - - def preprocess_order(self): - levels = {} - - for r in self.partial_order: - l = self.result[r]['x'] - levels.setdefault(l,[]) - levels[l].append(r) - - self.levels = levels - - - def graph_order(self): - """Finds actual-order of the nodes with respect to maximum number of nodes in a rank in component - """ - mid_pos = 0.0 - max_level = max(len(x) for x in self.levels.values()) - - for level in self.levels: - if level: - no = len(self.levels[level]) - factor = (max_level - no) * 0.10 - list = self.levels[level] - list.reverse() - - if no%2==0: - first_half = list[no // 2:] - factor = -factor - else: - first_half = list[no // 2 + 1:] - if max_level==1:#for the case when horizontal graph is there - self.result[list[no // 2]]['y'] = mid_pos + (self.result[list[no // 2]]['x']%2 * 0.5) - else: - self.result[list[no // 2]]['y'] = mid_pos + factor - - last_half = list[:no // 2] - - i=1 - for node in first_half: - self.result[node]['y'] = mid_pos - (i + factor) - i += 1 - - i=1 - for node in last_half: - self.result[node]['y'] = mid_pos + (i + factor) - i += 1 - else: - self.max_order += max_level+1 - mid_pos = self.result[self.start]['y'] - - - def tree_order(self, node, last=0): - mid_pos = self.result[node]['y'] - l = self.transitions.get(node, []) - l.reverse() - no = len(l) - - rest = no%2 - first_half = l[no // 2 + rest:] - last_half = l[:no // 2] - - for i, child in enumerate(first_half): - self.result[child]['y'] = mid_pos - (i+1 - (0 if rest else 0.5)) - - if self.transitions.get(child, False): - if last: - self.result[child]['y'] = last + len(self.transitions[child])/2 + 1 - last = self.tree_order(child, last) - - if rest: - mid_node = l[no // 2] - self.result[mid_node]['y'] = mid_pos - - if self.transitions.get(mid_node, False): - if last: - self.result[mid_node]['y'] = last + len(self.transitions[mid_node])/2 + 1 - if node!=mid_node: - last = self.tree_order(mid_node) - else: - if last: - self.result[mid_node]['y'] = last + 1 - self.result[node]['y'] = self.result[mid_node]['y'] - mid_pos = self.result[node]['y'] - - i=1 - last_child = None - for child in last_half: - self.result[child]['y'] = mid_pos + (i - (0 if rest else 0.5)) - last_child = child - i += 1 - if self.transitions.get(child, False): - if last: - self.result[child]['y'] = last + len(self.transitions[child])/2 + 1 - if node!=child: - last = self.tree_order(child, last) - - if last_child: - last = self.result[last_child]['y'] - - return last - - - def process_order(self): - """Finds actual-order of the nodes with respect to maximum number of nodes in a rank in component - """ - - if self.Is_Cyclic: - max_level = max(len(x) for x in self.levels.values()) - - if max_level%2: - self.result[self.start]['y'] = (max_level+1)/2 + self.max_order + (self.max_order and 1) - else: - self.result[self.start]['y'] = max_level /2 + self.max_order + (self.max_order and 1) - - self.graph_order() - - else: - self.result[self.start]['y'] = 0 - self.tree_order(self.start, 0) - min_order = math.fabs(min(x['y'] for x in self.result.values())) - - index = self.start_nodes.index(self.start) - same = False - - roots = [] - if index>0: - for start in self.start_nodes[:index]: - same = True - for edge in self.tree_list[start][1:]: - if edge in self.tree_list[self.start]: - continue - else: - same = False - break - if same: - roots.append(start) - - if roots: - min_order += self.max_order - else: - min_order += self.max_order + 1 - - for level in self.levels: - for node in self.levels[level]: - self.result[node]['y'] += min_order - - if roots: - roots.append(self.start) - one_level_el = self.tree_list[self.start][0][1] - base = self.result[one_level_el]['y']# * 2 / (index + 2) - - - no = len(roots) - first_half = roots[:no // 2] - - if no%2==0: - last_half = roots[no // 2:] - else: - last_half = roots[no // 2 + 1:] - - factor = -math.floor(no // 2) - for start in first_half: - self.result[start]['y'] = base + factor - factor += 1 - - if no%2: - self.result[roots[no // 2]]['y'] = base + factor - factor +=1 - - for start in last_half: - self.result[start]['y'] = base + factor - factor += 1 - - self.max_order = max(x['y'] for x in self.result.values()) - - def find_starts(self): - """Finds other start nodes of the graph in the case when graph is disconnected - """ - rem_nodes = [] - for node in self.nodes: - if not self.partial_order.get(node): - rem_nodes.append(node) - cnt = 0 - while True: - if len(rem_nodes)==1: - self.start_nodes.append(rem_nodes[0]) - break - else: - count = 0 - new_start = rem_nodes[0] - largest_tree = [] - - for node in rem_nodes: - self.partial_order = {} - tree = self.make_acyclic(None, node, 0, []) - if len(tree)+1 > count: - count = len(tree) + 1 - new_start = node - largest_tree = tree - else: - if not largest_tree: - new_start = rem_nodes[0] - rem_nodes.remove(new_start) - - self.start_nodes.append(new_start) - - - for edge in largest_tree: - if edge[0] in rem_nodes: - rem_nodes.remove(edge[0]) - if edge[1] in rem_nodes: - rem_nodes.remove(edge[1]) - - if not rem_nodes: - break - - - def rank(self): - """Finds the optimized rank of the nodes using Network-simplex algorithm - """ - self.levels = {} - self.critical_edges = [] - self.partial_order = {} - self.links = [] - self.Is_Cyclic = False - - self.tree_list[self.start] = self.make_acyclic(None, self.start, 0, []) - self.Is_Cyclic = self.rev_edges(self.tree_list[self.start]) - self.process_ranking(self.start) - self.init_rank() - - #make cut values of all tree edges to 0 to optimize feasible tree - e = self.leave_edge() - - while e : - f = self.enter_edge(e) - if e==f: - self.critical_edges.append(e) - else: - self.exchange(e,f) - e = self.leave_edge() - - #finalize rank using optimum feasible tree -# self.optimal_edges = {} -# for edge in self.tree_edges: -# source = self.optimal_edges.setdefault(edge[0], []) -# source.append(edge[1]) - -# self.finalize_rank(self.start, 0) - - #normalization - self.normalize() - for edge in self.edge_wt: - self.edge_wt[edge] = self.result[edge[1]]['x'] - self.result[edge[0]]['x'] - - def order_in_rank(self): - """Finds optimized order of the nodes within their ranks using median heuristic - """ - - self.make_chain() - self.preprocess_order() - self.order = {} - max_rank = max(x for x in self.levels) - - for i in range(max_rank+1): - self.order[i] = 0 - - self.init_order(self.start, self.result[self.start]['x']) - - for level in self.levels: - self.levels[level].sort(key=lambda x: self.result[x]['y']) - - self.order_heuristic() - self.process_order() - - def process(self, starting_node): - """Process the graph to find ranks and order of the nodes - - @param starting_node node from where to start the graph search - """ - - self.start_nodes = starting_node or [] - self.partial_order = {} - self.links = [] - self.tree_list = {} - - if self.nodes: - if self.start_nodes: - #add dummy edges to the nodes which does not have any incoming edges - tree = self.make_acyclic(None, self.start_nodes[0], 0, []) - - for node in self.no_ancester: - for sec_node in self.transitions.get(node, []): - if sec_node in self.partial_order: - self.transitions[self.start_nodes[0]].append(node) - break - - self.partial_order = {} - tree = self.make_acyclic(None, self.start_nodes[0], 0, []) - - - # if graph is disconnected or no start-node is given - #than to find starting_node for each component of the node - if len(self.nodes) > len(self.partial_order): - self.find_starts() - - self.max_order = 0 - #for each component of the graph find ranks and order of the nodes - for s in self.start_nodes: - self.start = s - self.rank() # First step:Netwoek simplex algorithm - self.order_in_rank() #Second step: ordering nodes within ranks - - - def __str__(self): - result = '' - for l in self.levels: - result += 'PosY: ' + str(l) + '\n' - for node in self.levels[l]: - result += '\tPosX: '+ str(self.result[node]['y']) + ' - Node:' + str(node) + "\n" - return result - - - def scale(self, maxx, maxy, nwidth=0, nheight=0, margin=20): - """Computes actual co-ordiantes of the nodes - """ - - #for flat edges ie. source an destination nodes are on the same rank - for src in self.transitions: - for des in self.transitions[src]: - if self.result[des]['x'] - self.result[src]['x'] == 0: - self.result[src]['x'] += 0.08 - self.result[des]['x'] -= 0.08 - - factorX = maxx + nheight - factorY = maxy + nwidth - - for node in self.result: - self.result[node]['y'] = (self.result[node]['y']) * factorX + margin - self.result[node]['x'] = (self.result[node]['x']) * factorY + margin - - - def result_get(self): - return self.result - -if __name__=='__main__': - starting_node = ['profile'] # put here nodes with flow_start=True - nodes = ['project','account','hr','base','product','mrp','test','profile'] - transitions = [ - ('profile','mrp'), - ('mrp','project'), - ('project','product'), - ('mrp','hr'), - ('mrp','test'), - ('project','account'), - ('project','hr'), - ('product','base'), - ('account','product'), - ('account','test'), - ('account','base'), - ('hr','base'), - ('test','base') - ] - - radius = 20 - g = graph(nodes, transitions) - g.process(starting_node) - g.scale(radius*3,radius*3, radius, radius) - - from PIL import Image - from PIL import ImageDraw - img = Image.new("RGB", (800, 600), "#ffffff") - draw = ImageDraw.Draw(img) - - result = g.result_get() - node_res = {} - for node in nodes: - node_res[node] = result[node] - - for name, node in node_res.items(): - - draw.arc( (int(node['y']-radius), int(node['x']-radius),int(node['y']+radius), int(node['x']+radius) ), 0, 360, (128,128,128)) - draw.text( (int(node['y']), int(node['x'])), str(name), (128,128,128)) - - - for t in transitions: - draw.line( (int(node_res[t[0]]['y']), int(node_res[t[0]]['x']),int(node_res[t[1]]['y']),int(node_res[t[1]]['x'])),(128,128,128) ) - img.save("graph.png", "PNG")