diff --git a/README.md b/README.md index f0636274..ef8f9095 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,28 @@ WSGIScriptAlias / /path_to_GraphSpace/graphspace/wsgi.py Refer to https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/modwsgi/ if any problems occur with the setup. +Documentation +================= + +GraphSpace has extensive documentation on the [user interface](http://docs.graphspace.org/en/latest/Quick_Tour_of_GraphSpace.html#welcome-screen), the [REST API](http://docs.graphspace.org/en/latest/Programmers_Guide.html#graphspace-rest-api) and a [Python package for programmatic interaction](http://manual.graphspace.org/projects/graphspace-python/en/latest/tutorial/index.html). + + Contributing ================= Feel free to fork and send us pull requests. Here are the [guidelines for contribution](https://github.com/Murali-group/GraphSpace/blob/master/CONTRIBUTING.md) in GraphSpace. + + +Contact +================= + +If you have questions or suggestions about GraphSpace, please contact + +- **T.M. Murali ([@tmmurali](https://github.com/tmmurali))** +- **Aditya Bharadwaj ([@adbharadwaj](https://github.com/adbharadwaj))** + + +License +================= + +GraphSpace is available under the GNU General Public License v2.0 license. See [LICENSE.md](https://github.com/Murali-group/GraphSpace/blob/master/LICENSE.md) for more information. diff --git a/api_specifications/api.raml b/api_specifications/api.raml index 1ca96ef4..fc254f80 100644 --- a/api_specifications/api.raml +++ b/api_specifications/api.raml @@ -543,6 +543,156 @@ types: graph_id: number positions_json: string style_json: string + GraphVersion: + description: Graph Version object + example: | + { + "name": "Graph Version 1.0", + "owner_email": "user1@example.com", + "description": "This is Version 1.0 of Sample Graph", + "graph_id": 1, + "graph_json": { + "elements": { + "nodes": [ + { + "position": { + "y": 277.5, + "x": 297.5 + }, + "data": { + "k": 0, + "id": "P4314611", + "name": "P4314611", + "label": "" + } + }, + { + "position": { + "y": 277.5, + "x": 892.5 + }, + "data": { + "k": 0, + "id": "P0810711", + "name": "P0810711", + "label": "" + } + } + ], + "edges": [ + { + "data": { + "target": "P0810711", + "k": 0, + "source": "P4314611", + "is_directed": 1, + "id": "P4314611-P0810711", + "name": "P4314611-P0810711" + }, + "style": { + "line-color": "blue", + "target-arrow-shape": "triangle", + "source-arrow-color": "yellow", + "width": "12px", + "curve-style": "bezier", + "line-style": "dotted" + } + } + ] + }, + "data": { + "description": "Description of graph.. can also point to an image hosted elsewhere", + "name": "My first graph", + "tags": [ + "tutorial", "sample" + ] + } + } + } + properties: + name: string + owner_email: string + description: string + graph_id: number + graph_json: GraphJSON + GraphVersionResponse: + description: Graph Version Response object + example: | + { + "name": "Graph Version 1.0", + "updated_at": "2017-03-25T15:37:20.728954", + "graph_id": 25, + "created_at": "2017-03-25T15:37:20.728954", + "owner_email": "user1@example.com", + "description": "This is Version 1.0 of Sample Graph", + "id": 21384, + "graph_json": { + "elements": { + "nodes": [ + { + "position": { + "y": 277.5, + "x": 297.5 + }, + "data": { + "k": 0, + "id": "P4314611", + "name": "P4314611", + "label": "" + } + }, + { + "position": { + "y": 277.5, + "x": 892.5 + }, + "data": { + "k": 0, + "id": "P0810711", + "name": "P0810711", + "label": "" + } + } + ], + "edges": [ + { + "data": { + "target": "P0810711", + "k": 0, + "source": "P4314611", + "is_directed": 1, + "id": "P4314611-P0810711", + "name": "P4314611-P0810711" + }, + "style": { + "line-color": "blue", + "target-arrow-shape": "triangle", + "source-arrow-color": "yellow", + "width": "12px", + "curve-style": "bezier", + "line-style": "dotted" + } + } + ] + }, + "data": { + "description": "Description of graph.. can also point to an image hosted elsewhere", + "name": "My first graph", + "tags": [ + "tutorial", "sample" + ] + } + } + } + properties: + id: number + name: string + owner_email: string + graph_id: number + graph_json: GraphJSON + created_at: string + updated_at: string + description: string Member: description: Member Response object example: | @@ -808,6 +958,117 @@ types: body: application/json: type: Error + /versions: + description: APIs to access versions of a specific graph on GraphSpace. + displayName: Graph Versions + get: + description: List all Graph Versions matching query criteria, if provided; otherwise list all Graph Versions. + queryParameters: + owner_email?: string + name?: + description: Search for Graph Versions with given name. In order to search for versions with given name as a substring, wrap the name with percentage symbol. For example, %xyz% will search for all versions with xyz in their name. + type: string + limit?: + description: Number of entities to return. + default: 20 + type: number + offset?: + description: Offset the list of returned entities by this number. + default: 0 + type: number + order?: + description: Defines the column sort order, can only be 'asc' or 'desc'. + type: string + sort?: + description: Defines which column will be sorted. + type: string + example: "name" + responses: + 200: + description: SUCCESS + body: + application/json: + type: object + properties: + total: number + versions: GraphVersionResponse[] + 400: + description: BAD REQUEST + body: + application/json: + type: Error + 403: + description: FORBIDDEN + body: + application/json: + type: Error + post: + description: Create a new Graph Version. + body: + application/json: + type: GraphVersion + responses: + 201: + description: SUCCESS + body: + application/json: + type: GraphVersionResponse + 400: + description: BAD REQUEST + body: + application/json: + type: Error + /{version_id}: + description: APIs to access a specific graph version on GraphSpace. + displayName: Graph Version + get: + description: Get a Graph Version by id + responses: + 200: + description: SUCCESS + body: + application/json: + type: GraphVersionResponse + 403: + description: FORBIDDEN + body: + application/json: + type: Error + put: + description: Update a Graph Version by id + body: + application/json: + type: GraphVersion + responses: + 200: + description: SUCCESS + body: + application/json: + type: GraphVersionResponse + 403: + description: FORBIDDEN + body: + application/json: + type: Error + delete: + description: Delete a Graph Version by id + responses: + 200: + description: SUCCESS + body: + application/json: + type: object + properties: + message: string + example: | + { + "message": "Successfully deleted graph version with id=21341" + } + 403: + description: FORBIDDEN + body: + application/json: + type: Error /groups: description: APIs to access groups on GraphSpace. diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 7f012a05..07a8a612 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -191,8 +191,18 @@ def add_graph(request, name=None, tags=None, is_public=None, graph_json=None, st # Construct new graph to add to database new_graph = db.add_graph(request.db_session, name=name, owner_email=owner_email, - graph_json=json.dumps(G.get_graph_json()), style_json=json.dumps(G.get_style_json()), is_public=is_public, default_layout_id=default_layout_id) + default_version = db.add_graph_version(request.db_session, name=name, description='Default Version', + owner_email=owner_email, graph_json=json.dumps(G.get_graph_json()), + style_json=json.dumps(G.get_style_json()), graph_id=new_graph.id, is_default = True) + + # Add default_version to new_graph + new_graph.__setattr__('default_version', default_version) + # Add graph_json to new_graph + new_graph.__setattr__('graph_json', default_version.graph_json) + # Add style_json to new_graph + new_graph.__setattr__('style_json', default_version.style_json) + # Add graph tags for tag in G.get_tags(): add_graph_tag(request, new_graph.id, tag) @@ -208,7 +218,7 @@ def add_graph(request, name=None, tags=None, is_public=None, graph_json=None, st @atomic_transaction def update_graph(request, graph_id, name=None, is_public=None, graph_json=None, style_json=None, owner_email=None, - default_layout_id=None): + default_layout_id=None, default_version_id=None): graph = {} if name is not None: graph['name'] = name @@ -219,6 +229,9 @@ def update_graph(request, graph_id, name=None, is_public=None, graph_json=None, if default_layout_id is not None: graph['default_layout_id'] = default_layout_id if default_layout_id != 0 else None + if default_version_id is not None: + graph['default_version_id'] = default_version_id if default_version_id != 0 else None + if style_json is not None: GSGraph.validate_style_json(style_json) graph['style_json'] = json.dumps(style_json) @@ -586,3 +599,225 @@ def add_edge(request, name=None, head_node_id=None, tail_node_id=None, is_direct def delete_edge_by_id(request, edge_id): db.delete_edge(request.db_session, id=edge_id) return + + +def get_graph_comparison(request, graph_1, graph_2, operation): + """ + Caller function to execute 2-Graph Intersection or Graph Difference operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + if operation == 'intersection': + return get_graphs_intersection(request, graph_1, graph_2) + else: + return get_graphs_difference(request, graph_1, graph_2) + + +def get_graphs_intersection(request, graph_1, graph_2): + """ + Caller function to execute 2-Graph Intersection operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_intersection(request.db_session, graph_1, graph_2) + edge_data = db.edges_intersection(request.db_session, graph_1, graph_2) + return node_data, edge_data + + +def get_graphs_difference(request, graph_1, graph_2): + """ + Caller function to execute 2-Graph Difference operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_difference(request.db_session, graph_1, graph_2) + edge_data = db.edges_difference(request.db_session, graph_1, graph_2) + return node_data, edge_data + + +def get_graph_comparison_multi(request, graphs, operation): + """ + Caller function to execute N-Graph Intersection or Graph Difference operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + if operation == 'intersection': + return get_graphs_intersection_multi(request, graphs) + else: + return get_graphs_difference_multi(request, graphs) + + +def get_graphs_intersection_multi(request, graphs): + """ + Caller function to execute N-Graph Intersection operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_intersection_multi(request.db_session, graphs) + edge_data = db.edges_intersection_multi(request.db_session, graphs) + return node_data, edge_data + + +def get_graphs_difference_multi(request, graphs): + """ + Caller function to execute N-Graph Difference operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_difference_multi(request.db_session, graphs) + edge_data = db.edges_difference_multi(request.db_session, graphs) + return node_data, edge_data + + +def search_graph_versions(request, graph_id=None, names=None, limit=20, offset=0, order='desc', sort='name'): + """ + Parameters + ---------- + request : object + HTTP GET Request. + graph_id : string + Unique ID of the graph. + names : list of strings + Search for graphs with given list of names. In order to search for graphs with given name as a substring, wrap the name with percentage symbol. For example, %xyz% will search for all graphs with xyz in their name. + limit : integer + Number of entities to return. Default value is 20. + offset : integer + Offset the list of returned entities by this number. Default value is 0. + order : string + Defines the column sort order, can only be 'asc' or 'desc'. + sort : string + Defines which column will be sorted. + + Returns + ------- + total : integer + Number of groups matching the request. + graph_versions : List of Graph Versions. + List of Graph Version Objects with given limit and offset. + + Raises + ------ + + Notes + ------ + """ + if sort == 'name': + sort_attr = db.GraphVersion.name + elif sort == 'update_at': + sort_attr = db.GraphVersion.updated_at + else: + sort_attr = db.GraphVersion.name + + if order == 'desc': + orber_by = db.desc(sort_attr) + else: + orber_by = db.asc(sort_attr) + + total, graph_versions = db.find_graph_versions(request.db_session, + names=names, + graph_id=graph_id, + limit=limit, + offset=offset, + order_by=orber_by) + + return total, graph_versions + + +def get_graph_version_by_id(request, version_id): + return db.get_graph_version_by_id(request.db_session, version_id) + + +def add_graph_version(request, name=None, description=None, owner_email=None, graph_json=None, graph_id=None, style_json=None): + if name is None or graph_id is None or graph_json is None: + raise Exception("Required Parameter is missing!") + return db.add_graph_version(request.db_session, name=name, description=description, owner_email=owner_email, graph_json=json.dumps(graph_json), style_json=json.dumps(style_json), graph_id=graph_id) + + +def delete_graph_version_by_id(request, graph_version_id): + db.delete_graph_version(request.db_session, id=graph_version_id) + return + + +def get_graph_version_to_layout_status(request, graph_version_id, layout_id): + return db.get_graph_version_to_layout_status(request.db_session, graph_version_id, layout_id) + + +def add_graph_version_to_layout_status(request, graph_version_id, layout_id, status=None): + if graph_version_id is None or layout_id is None: + raise Exception("Required Parameter(s) are missing!") + return db.add_graph_version_to_layout_status(request.db_session, graph_version_id=graph_version_id, layout_id=layout_id, status=status) + + +def update_graph_version_to_layout_status(request, graph_version_id, layout_id=None, status=None): + return db.update_graph_version_to_layout_status(request.db_session, graph_version_id, layout_id, status) + + +def delete_graph_version_to_layout_status(request, graph_version_id, layout_id): + db.delete_graph_version_to_layout_status(request.db_session, graph_version_id=graph_version_id, layout_id=layout_id) + return diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 388b8141..c088608a 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -3,7 +3,7 @@ from applications.users.models import * from graphspace.wrappers import with_session -from sqlalchemy.orm import defer, undefer +from sqlalchemy.orm import defer, undefer, aliased @with_session @@ -18,8 +18,8 @@ def get_edges(db_session, edges, order=desc(Edge.updated_at), page=0, page_size= @with_session def get_graphs_by_edges_and_nodes_and_names(db_session, group_ids=None, names=None, nodes=None, edges=None, tags=None, - order=desc(Graph.updated_at), page=0, page_size=10, partial_matching=False, - owner_email=None, is_public=None): + order=desc(Graph.updated_at), page=0, page_size=10, partial_matching=False, + owner_email=None, is_public=None): query = db_session.query(Graph) edges = [] if edges is None else edges @@ -47,7 +47,7 @@ def get_graphs_by_edges_and_nodes_and_names(db_session, group_ids=None, names=No nodes_filter_group = [Node.label.ilike(node) for node in nodes] nodes_filter_group.extend([Node.name.ilike(node) for node in nodes]) edges_filter_group = [and_(Edge.head_node.has(Node.name.ilike(u)), Edge.tail_node.has(Node.name.ilike(v))) for u, v - in edges] + in edges] edges_filter_group.extend( [and_(Edge.tail_node.has(Node.name.ilike(u)), Edge.head_node.has(Node.name.ilike(v))) for u, v in edges]) edges_filter_group.extend( @@ -80,9 +80,9 @@ def get_graphs_by_edges_and_nodes_and_names(db_session, group_ids=None, names=No @with_session -def add_graph(db_session, name, owner_email, graph_json, style_json, is_public=0, default_layout_id=None): - graph = Graph(name=name, owner_email=owner_email, graph_json=graph_json, style_json=style_json, is_public=is_public, - default_layout_id=default_layout_id) +def add_graph(db_session, name, owner_email, is_public=0, default_layout_id=None): + graph = Graph(name=name, owner_email=owner_email, is_public=is_public, + default_layout_id=default_layout_id) db_session.add(graph) return graph @@ -97,7 +97,12 @@ def update_graph(db_session, id, updated_graph): :return: Graph if id exists else None """ graph = db_session.query(Graph).filter(Graph.id == id).one_or_none() + version_id = update_graph if 'default_version_id' in updated_graph else graph.default_version_id + graph_version = db_session.query(GraphVersion).filter(GraphVersion.id == version_id) + for (key, value) in updated_graph.items(): + if key == 'graph_json' or key == 'style_json': + setattr(graph_version, key, value) setattr(graph, key, value) return graph @@ -122,15 +127,17 @@ def delete_graph(db_session, id): @with_session def get_graph_by_id(db_session, id): - return db_session.query(Graph).filter(Graph.id == id).one_or_none() + query = db_session.query(Graph).filter(Graph.id == id) + query.options(joinedload('graph_versions')).filter(GraphVersion.id==Graph.default_version_id) + return query.one_or_none() @with_session def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is_public=None, names=None, nodes=None, - edges=None, - tags=None, limit=None, offset=None, order_by=desc(Graph.updated_at)): + edges=None, + tags=None, limit=None, offset=None, order_by=desc(Graph.updated_at)): query = db_session.query(Graph) - query = query.options(defer("graph_json")).options(defer("style_json")) + #query = query.options(defer("graph_json")).options(defer("style_json")) graph_filter_group = [] if is_public is not None: @@ -145,6 +152,7 @@ def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is query = query.filter(Graph.id.in_(graph_ids)) options_group = [] + options_group.append(joinedload('graph_versions')) if tags is not None and len(tags) > 0: options_group.append(joinedload('graph_tags')) if nodes is not None and len(nodes) > 0: @@ -158,6 +166,7 @@ def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is if group_ids is not None: query = query.filter(Graph.groups.any(Group.id.in_(group_ids))) + query = query.filter(GraphVersion.id==Graph.default_version_id) edges = [] if edges is None else edges nodes = [] if nodes is None else nodes @@ -217,10 +226,10 @@ def add_edge(db_session, graph_id, head_node_id, tail_node_id, name, is_directed tail_node = get_node_by_id(db_session, tail_node_id) edge = Edge(name=name, graph_id=graph_id, - head_node_id=head_node_id, tail_node_id=tail_node_id, - head_node_name=head_node.name, tail_node_name=tail_node.name, - head_node_label=head_node.label, tail_node_label=tail_node.label, - is_directed=is_directed) + head_node_id=head_node_id, tail_node_id=tail_node_id, + head_node_name=head_node.name, tail_node_name=tail_node.name, + head_node_label=head_node.label, tail_node_label=tail_node.label, + is_directed=is_directed) db_session.add(edge) return edge @@ -314,7 +323,7 @@ def delete_graph_to_group(db_session, group_id, graph_id): @with_session def find_layouts(db_session, owner_email=None, is_shared=None, name=None, graph_id=None, limit=None, offset=None, - order_by=desc(Layout.updated_at)): + order_by=desc(Layout.updated_at)): query = db_session.query(Layout) if order_by is not None: @@ -364,7 +373,7 @@ def add_layout(db_session, owner_email, name, graph_id, is_shared, style_json, p """ layout = Layout(owner_email=owner_email, name=name, graph_id=graph_id, is_shared=is_shared, style_json=style_json, - positions_json=positions_json) + positions_json=positions_json) db_session.add(layout) return layout @@ -387,6 +396,9 @@ def update_layout(db_session, id, updated_layout): layout = db_session.query(Layout).filter(Layout.id == id).one_or_none() for (key, value) in updated_layout.items(): setattr(layout, key, value) + layout_to_graph_versions = db_session.query(LayoutToGraphVersion).filter(LayoutToGraphVersion.layout_id == 7).all() + for obj in layout_to_graph_versions: + obj.status = "Null" return layout @@ -405,7 +417,7 @@ def delete_layout(db_session, id): @with_session def find_nodes(db_session, labels=None, names=None, graph_id=None, limit=None, offset=None, - order_by=desc(Node.updated_at)): + order_by=desc(Node.updated_at)): query = db_session.query(Node) if graph_id is not None: @@ -430,7 +442,7 @@ def find_nodes(db_session, labels=None, names=None, graph_id=None, limit=None, o @with_session def find_edges(db_session, is_directed=None, names=None, edges=None, graph_id=None, limit=None, offset=None, - order_by=desc(Node.updated_at)): + order_by=desc(Node.updated_at)): query = db_session.query(Edge) if graph_id is not None: @@ -458,3 +470,396 @@ def find_edges(db_session, is_directed=None, names=None, edges=None, graph_id=No query = query.limit(limit).offset(offset) return total, query.all() + + +@with_session +def nodes_intersection(db_session, graph_1_id=None, graph_2_id=None): + """ + Find node intersection of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ + alias_node = aliased(Node) + query = db_session.query(Node, alias_node) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Node.graph_id == graph_1_id).\ + join(alias_node, Node.name == alias_node.name).\ + filter(alias_node.graph_id == graph_2_id) + total = query.count() + return total, query.all() + + +@with_session +def nodes_difference(db_session, graph_1_id=None, graph_2_id=None): + """ + Find node difference of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ + graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) + graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Node.name).\ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + +@with_session +def edges_intersection(db_session, graph_1_id=None, graph_2_id=None): + """ + Find edge intersection of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ + alias_edge = aliased(Edge) + query = db_session.query(Edge, alias_edge) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Edge.graph_id == graph_1_id).\ + join(alias_edge, Edge.head_node_name == alias_edge.head_node_name).\ + filter(alias_edge.graph_id == graph_2_id).\ + filter(Edge.tail_node_name == alias_edge.tail_node_name) + total = query.count() + return total, query.all() + + +@with_session +def edges_difference(db_session, graph_1_id=None, graph_2_id=None): + """ + Find edge difference of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ + graph1_query = db_session.query(Edge).filter(Edge.graph_id == graph_1_id) + graph2_query = db_session.query(Edge).filter(Edge.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Edge.name). \ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + +@with_session +def nodes_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): + """ + Find node subquery of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :param operation: Comparison operation - difference or intersection.. + :return: query.subquery object. + """ + alias_node1 = aliased(Node, name='n1') + alias_node2 = aliased(Node, name='n2') + if operation == 'intersection': + query = db_session.query(alias_node1, alias_node2) + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(alias_node1.graph_id == graph_1_id). \ + join(alias_node2, alias_node1.name == alias_node2.name). \ + filter(alias_node2.graph_id == graph_2_id) + n1 = query.subquery(name='s1', with_labels=True) + db_session.query(Node).filter(Node.graph_id == 6).join(n1, n1.c.name == Node.name) + return query.subquery(name='s1', with_labels=True) + else: + graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) + graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Node.name). \ + filter(sub_q.c.name == None) + return query.subquery() + +@with_session +def edges_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): + """ + Find node subquery of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :param operation: Comparison operation - difference or intersection.. + :return: query.subquery object. + """ + alias_edge = aliased(Edge) + if operation == 'intersection': + query = db_session.query(Edge, alias_edge) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Edge.graph_id == graph_1_id). \ + join(alias_edge, Edge.head_node_name == alias_edge.head_node_name). \ + filter(alias_edge.graph_id == graph_2_id). \ + filter(Edge.tail_node_name == alias_edge.tail_node_name) + return query.subquery + else: + graph1_query = db_session.query(Edge).filter(Edge.graph_id == graph_1_id) + graph2_query = db_session.query(Edge).filter(Edge.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Edge.name). \ + filter(sub_q.c.name == None) + return query.subquery + + +@with_session +def nodes_intersection_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Node).filter(Node.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Node).filter(Node.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.name == Node.name) + total = query.count() + return total, query.all() + + +@with_session +def nodes_difference_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Node).filter(Node.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Node).filter(Node.graph_id == graph_id).subquery() + query = query.outerjoin(sub_q, sub_q.c.name == Node.name).\ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + +@with_session +def edges_intersection_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Edge).filter(Edge.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Edge).filter(Edge.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.head_node_name == Edge.head_node_name)\ + .filter(Edge.tail_node_name == sub_q.c.tail_node_name) + total = query.count() + return total, query.all() + + +@with_session +def edges_difference_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Edge).filter(Edge.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Edge).filter(Edge.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.head_node_name == Edge.head_node_name)\ + .filter(Edge.tail_node_name == sub_q.c.tail_node_name).filter(sub_q.c.head_node_name == None) + total = query.count() + return total, query.all() + + +@with_session +def nodes_comparison(db_session, comp_expression=None): + """ + Work In Progress. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + # Infix -> a 'i' ( b - c ) + # Postfix -> a b c - 'i' + + comp_expression = [1, 2, 5, 'd', 'i'] + temp_stack = [] + for item in comp_expression: + if item not in ['d','i']: + temp_stack.append(item) + elif item in ['d','i']: + query1 = temp_stack.pop() + query2 = temp_stack.pop() + if type(query1) != int: + query = db_session.query(Node).filter(Node.graph_id == query2) + sub_query = query.outerjoin(query1, query1.c.name == Node.name). \ + filter(query1.c.name == None) + else: + sub_query = nodes_subquery(db_session, query1, query2, item) + temp_stack.append(sub_query) + query = temp_stack.pop() + count = query.count() + + return count, query.all() + + +@with_session +def find_graph_versions(db_session, names=None, graph_id=None, limit=None, offset=None, + order_by=desc(GraphVersion.updated_at)): + """ + Find graph version by Graph ID. + :param db_session: Database session. + :param graph_id: Unique ID of the graph + :param name - Name of the graph version + :param limit - Number of entities to return. Default value is 20. + :param offset - Offset the list of returned entities by this number. Default value is 0. + :param order_by - Defines which column the results will be sorted by. + :return: Total, Graph Versions + """ + + query = db_session.query(GraphVersion) + + if graph_id is not None: + query = query.filter(GraphVersion.graph_id == graph_id) + + names = [] if names is None else names + if len(names) > 0: + query = query.filter( + or_(*([GraphVersion.name.ilike(name) for name in names]))) + + total = query.count() + + if order_by is not None: + query = query.order_by(order_by) + + if offset is not None and limit is not None: + query = query.limit(limit).offset(offset) + + return total, query.all() + +@with_session +def get_graph_version_by_id(db_session, id): + """ + Get graph version by ID. + :param db_session: Database session. + :param id: Unique ID of the graph version + :return: Graph Version if id exists else None + """ + return db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() + +@with_session +def add_graph_version(db_session, graph_id, name, graph_json, owner_email, style_json=None, description=None, is_default=None): + """ + Get graph version by ID. + :param db_session: Database session. + :param graph_id: Unique ID of the graph + :param name - Name of the graph version + :param graph_json - positions_json of the layouts. + :param owner_email - ID of user who owns the graph + :param style_json - style_json of the layouts. + :param description - ID of the graph the layout belongs to. + :param is_default - Set this graph_version as the default version of the graph + :return: Graph Version if id exists else None + """ + graph_version = GraphVersion(name=name, graph_id=graph_id, graph_json=graph_json, style_json=style_json, owner_email=owner_email, description=description) + if is_default is not None: + set_default_version(db_session, graph_id, graph_version.id) + db_session.add(graph_version) + return graph_version + +@with_session +def delete_graph_version(db_session, id): + """ + Delete graph version. + :param db_session: Database session. + :param id: Unique ID of the graph version + :return: None + """ + graph_version = db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() + db_session.delete(graph_version) + return + +@with_session +def set_default_version(db_session, graph_id, default_version_id): + """ + Set the default graph version. + :param db_session: Database session. + :param graph_id: Unique ID of the graph + :param default_version_id: Unique ID of the graph version + :return: None + """ + graph = db_session.query(Graph).filter(Graph.id == graph_id).one_or_none() + setattr(graph, 'default_version_id', default_version_id) + return + + +@with_session +def get_graph_version_to_layout_status(db_session, graph_version_id, layout_id): + """ + GET graph version to layout compatibility status. + :param db_session: Database session. + :param graph_version_id: Unique ID of the graph version + :param layout_id: Unique ID of the layout + :return: Graph Version to Layout compatibility status if it exists else None + """ + return db_session.query(LayoutToGraphVersion).filter(and_(LayoutToGraphVersion.graph_version_id == graph_version_id, LayoutToGraphVersion.layout_id == layout_id)).one_or_none() + + +@with_session +def add_graph_version_to_layout_status(db_session, graph_version_id, layout_id, status=None): + """ + ADD graph version to layout compatibility. + :param db_session: Database session. + :param graph_version_id: Unique ID of the graph version + :param layout_id - Unique ID of the layout. + :param status - Compatibility status. [Default = None]. + :return: Graph Version to Layout compatibility status if it exists else None + """ + graph_version_to_layout = LayoutToGraphVersion(graph_version_id=graph_version_id, layout_id=layout_id, status=status) + db_session.add(graph_version_to_layout) + return graph_version_to_layout + + +@with_session +def delete_graph_version_to_layout_status(db_session, graph_version_id, layout_id): + """ + DELETE graph version. + :param db_session: Database session. + :param id: Unique ID of the graph version + :return: None + """ + graph_version_to_layout = db_session.query(LayoutToGraphVersion).filter(and_(LayoutToGraphVersion.graph_version_id == graph_version_id, + LayoutToGraphVersion.layout_id == layout_id)).one_or_none() + db_session.delete(graph_version_to_layout) + return + + +@with_session +def update_graph_version_to_layout_status(db_session, graph_version_id, layout_id=None, status=None): + """ + UPDATE graph version to layout compatibility. + :param db_session: Database session. + :param graph_version_id: Unique ID of the graph version + :param layout_id - Unique ID of the layout. + :param status - Compatibility status. [Default = None]. + :return: Graph Version to Layout compatibility status if layout_id is passed and row exists else None + """ + + query = db_session.query(LayoutToGraphVersion).filter(LayoutToGraphVersion.graph_version_id == graph_version_id) + if layout_id: + query = query.filter(LayoutToGraphVersion.layout_id == layout_id) + + graph_version_to_layout = query.all() + + for item in graph_version_to_layout: + item.status = status + # if len(graph_version_to_layout) >1: + # return {"message": "Updated status of %d Layouts" % (len(graph_version_to_layout))} + return graph_version_to_layout[0] diff --git a/applications/graphs/models.py b/applications/graphs/models.py index f0e3e554..0bb9894f 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -17,8 +17,8 @@ class Graph(IDMixin, TimeStampMixin, Base): name = Column(String, nullable=False) owner_email = Column(String, ForeignKey('user.email', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) - graph_json = Column(String, nullable=False) - style_json = Column(String, nullable=False) + default_version_id = Column(Integer, ForeignKey('graph_version.id', ondelete="CASCADE", onupdate="CASCADE"), + nullable=True) is_public = Column(Integer, nullable=False, default=0) default_layout_id = Column(Integer, ForeignKey('layout.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=True) @@ -32,6 +32,11 @@ class Graph(IDMixin, TimeStampMixin, Base): edges = relationship("Edge", back_populates="graph", cascade="all, delete-orphan") nodes = relationship("Node", back_populates="graph", cascade="all, delete-orphan") + default_version = relationship("GraphVersion", foreign_keys=[default_version_id], back_populates="default_version_graph", + uselist=False) + graph_versions = relationship("GraphVersion", foreign_keys="GraphVersion.graph_id", back_populates="graph", + passive_deletes=True) + groups = association_proxy('shared_with_groups', 'group') tags = association_proxy('graph_tags', 'tag') @@ -56,6 +61,7 @@ def serialize(cls, **kwargs): 'is_public': cls.is_public, 'tags': [tag.name for tag in cls.tags], 'default_layout_id': cls.default_layout_id, + 'default_version_id': cls.default_version_id, 'created_at': cls.created_at.isoformat(), 'updated_at': cls.updated_at.isoformat() } @@ -64,11 +70,12 @@ def serialize(cls, **kwargs): 'id': cls.id, 'owner_email': cls.owner_email, 'name': cls.name, - 'graph_json': json.loads(cls.graph_json), - 'style_json': json.loads(cls.style_json), + 'graph_json': json.loads(cls.default_version.graph_json), + 'style_json': json.loads(cls.default_version.style_json) if cls.default_version.style_json else {}, 'is_public': cls.is_public, 'tags': [tag.name for tag in cls.tags], 'default_layout_id': cls.default_layout_id, + 'default_version_id': cls.default_version_id, 'created_at': cls.created_at.isoformat(), 'updated_at': cls.updated_at.isoformat() } @@ -279,3 +286,74 @@ class GraphToTag(TimeStampMixin, Base): def __table_args__(cls): args = cls.constraints + cls.indices return args + + +class GraphVersion(IDMixin, TimeStampMixin, Base): + __tablename__ = 'graph_version' + + name = Column(String, nullable=False) + graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + owner_email = Column(String, ForeignKey('user.email', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + graph_json = Column(String, nullable=False) + style_json = Column(String, nullable=False) + description = Column(String, nullable=True) + graph = relationship("Graph", foreign_keys=[graph_id], back_populates="graph_versions", uselist=False) + default_version_graph = relationship("Graph", foreign_keys="Graph.default_version_id", + back_populates="default_version", cascade="all, delete-orphan", + uselist=False) + constraints = ( + UniqueConstraint('graph_id', 'name', name='_graph_version_uc_graph_id_name'), + UniqueConstraint('id', 'name', name='_graph_version_uc_id_name'), + ) + + indices = ( + Index('graph_version_idx_name', text("name gin_trgm_ops"), postgresql_using="gin"), + ) + + @declared_attr + def __table_args__(cls): + args = cls.constraints + cls.indices + return args + + def serialize(cls, **kwargs): + if 'summary' in kwargs and kwargs['summary']: + return { + 'id': cls.id, + 'name': cls.name, + 'description': cls.description, + 'creator': cls.owner_email, + 'created_at': cls.created_at.isoformat(), + 'updated_at': cls.updated_at.isoformat() + } + else: + return { + 'id': cls.id, + 'name': cls.name, + 'description': cls.description, + 'graph_json': cls.graph_json, + 'style_json': cls.style_json, + 'creator': cls.owner_email, + 'created_at': cls.created_at.isoformat(), + 'updated_at': cls.updated_at.isoformat() + } + + +class LayoutToGraphVersion(IDMixin, TimeStampMixin, Base): + __tablename__ = 'layout_to_graph_version' + + layout_id = Column(Integer, ForeignKey('layout.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True) + graph_version_id = Column(Integer, ForeignKey('graph_version.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True) + status = Column(String, nullable=True) + + constraints = ( + UniqueConstraint('layout_id', 'graph_version_id', 'compatibility_status', name='layout_uc_layout_id_graph_version_id_compatibility_status') + ) + + def serialize(cls, **kwargs): + return { + 'graph_version_id': cls.graph_version_id, + 'layout_id': cls.layout_id, + 'status': cls.status, + 'created_at': cls.created_at.isoformat(), + 'updated_at': cls.updated_at.isoformat() + } diff --git a/applications/graphs/tests.py b/applications/graphs/tests.py index f05abfe7..ee40488c 100644 --- a/applications/graphs/tests.py +++ b/applications/graphs/tests.py @@ -37,7 +37,7 @@ def test_crud_operation(self): # Create self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.assertEqual(graph1.name, 'graph1') @@ -61,23 +61,23 @@ def test_crud_operation(self): def test_owner_email_fkey_constraint(self): with self.assertRaises(IntegrityError): - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() def test_default_layout_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0, default_layout_id=1)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0, default_layout_id=1)) self.session.commit() def test_name_owner_email_uc_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=1)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=1)) self.session.commit() def test_cascade_on_user_delete(self): @@ -85,7 +85,7 @@ def test_cascade_on_user_delete(self): On deleting user row, the corresponding row in graph table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() @@ -101,7 +101,7 @@ def test_cascade_on_user_update(self): On deleting user row, the corresponding row in graph table should also be updated """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() graph1 = self.session.query(Graph).filter(and_(Graph.owner_email == 'owner@example.com', Graph.name == 'graph1')).one_or_none() @@ -115,7 +115,7 @@ def test_cascade_on_user_update(self): def test_owner_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) self.session.commit() graph1 = self.session.query(Graph).filter(and_(Graph.owner_email == 'owner@example.com', Graph.name == 'graph1')).one_or_none() @@ -125,7 +125,7 @@ def test_owner_relationship(self): def test_nodes_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source1')) self.session.commit() @@ -140,7 +140,7 @@ def test_nodes_relationship(self): def test_edges_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -154,9 +154,9 @@ def test_edges_relationship(self): def test_layouts_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}')) self.session.commit() layout1 = self.session.query(Layout).filter(Layout.name == 'layout1').one_or_none() @@ -165,9 +165,9 @@ def test_layouts_relationship(self): def test_default_layout_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() layout1 = self.session.query(Layout).filter(Layout.name == 'layout1').one_or_none() @@ -188,7 +188,7 @@ def test_groups_relationship(self): Group.owner_email == 'owner@example.com', Group.name == 'group1')).one_or_none() self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -197,7 +197,7 @@ def test_groups_relationship(self): def test_tags_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -235,7 +235,7 @@ def test_crud_operation(self): # Create self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -272,7 +272,7 @@ def test_graph_id_name_uc_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source1')) self.session.add(Node(graph_id=graph1.id, name='source', label='source2')) @@ -283,7 +283,7 @@ def test_cascade_on_user_delete(self): On deleting user row, the corresponding row in node table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source1')) self.session.commit() @@ -302,7 +302,7 @@ def test_cascade_on_graph_delete(self): On deleting graph row, the corresponding row in node table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source1')) self.session.commit() @@ -317,7 +317,7 @@ def test_cascade_on_graph_delete(self): def test_graph_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source1')) self.session.commit() @@ -329,7 +329,7 @@ def test_graph_relationship(self): def test_source_edges_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -345,7 +345,7 @@ def test_source_edges_relationship(self): def test_target_edges_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -388,7 +388,7 @@ def test_crud_operation(self): # Create self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -419,7 +419,7 @@ def test_graph_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -435,7 +435,7 @@ def test_head_node_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -451,7 +451,7 @@ def test_tail_node_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -467,7 +467,7 @@ def test_graph_id_name_uc_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -481,7 +481,7 @@ def test_graph_id_head_node_id_tail_node_id_uc_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -496,7 +496,7 @@ def test_cascade_on_user_delete(self): On deleting user row, the corresponding row in edge table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -519,7 +519,7 @@ def test_cascade_on_graph_delete(self): On deleting graph row, the corresponding row in edge table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -541,7 +541,7 @@ def test_cascade_on_node_delete(self): On deleting node row, the corresponding row in edge table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -560,7 +560,7 @@ def test_cascade_on_node_delete(self): def test_graph_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -574,7 +574,7 @@ def test_graph_relationship(self): def test_head_node_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -591,7 +591,7 @@ def test_head_node_relationship(self): def test_tail_node_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(Node(graph_id=graph1.id, name='source', label='source')) self.session.add(Node(graph_id=graph1.id, name='target', label='target')) @@ -644,7 +644,7 @@ def test_add_delete_operation(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) @@ -676,7 +676,7 @@ def test_graph_id_fkey_constraint(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.delete(graph1) self.session.commit() @@ -700,7 +700,7 @@ def test_group_id_fkey_constraint(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.delete(group1) self.session.commit() @@ -723,7 +723,7 @@ def test_graph_id_group_id_unique_constraint(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -745,7 +745,7 @@ def test_cascade_on_group_member_delete(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -777,7 +777,7 @@ def test_cascade_on_group_owner_delete(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='member@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='member@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'member@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -808,7 +808,7 @@ def test_cascade_on_graph_owner_delete(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) self.session.add(GroupToUser(user_id=graph_owner.id, group_id=group1.id)) - self.session.add(Graph(name='graph1', owner_email='graph_owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='graph_owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'graph_owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -836,7 +836,7 @@ def test_cascade_on_graph_delete(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -865,7 +865,7 @@ def test_cascade_on_group_delete(self): self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) group2user1 = self.session.query(GroupToUser).filter(and_(GroupToUser.user_id == member.id, GroupToUser.group_id == group1.id)).one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -891,7 +891,7 @@ def test_graph_relationship(self): Group.owner_email == 'owner@example.com', Group.name == 'group1')).one_or_none() self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -909,7 +909,7 @@ def test_group_relationship(self): Group.owner_email == 'owner@example.com', Group.name == 'group1')).one_or_none() self.session.add(GroupToUser(user_id=member.id, group_id=group1.id)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GroupToGraph(graph_id=graph1.id, group_id=group1.id)) self.session.commit() @@ -977,7 +977,7 @@ def test_name_uc_constraint(self): def test_graphs_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1016,7 +1016,7 @@ def test_add_delete_operation(self): # Create self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1040,7 +1040,7 @@ def test_graph_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1058,7 +1058,7 @@ def test_tag_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1074,7 +1074,7 @@ def test_graph_id_tag_id_unique_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1093,7 +1093,7 @@ def test_cascade_on_graph_delete(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1117,7 +1117,7 @@ def test_cascade_on_graph_delete(self): def test_cascade_on_tag_delete(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1142,7 +1142,7 @@ def test_cascade_on_graph_owner_delete(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1166,7 +1166,7 @@ def test_cascade_on_graph_owner_delete(self): def test_graph_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1179,7 +1179,7 @@ def test_graph_relationship(self): def test_tag_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.add(GraphTag(name='tag1')) @@ -1219,9 +1219,9 @@ def test_crud_operation(self): # Create self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() layout1 = self.session.query(Layout).filter(Layout.owner_email == 'owner@example.com').one_or_none() @@ -1247,36 +1247,36 @@ def test_graph_id_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.delete(graph1) self.session.commit() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() def test_owner_email_fkey_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() self.session.delete(owner) self.session.commit() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() def test_name_graph_id_owner_email_uc_constraint(self): with self.assertRaises(IntegrityError): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() self.session.commit() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{"a": "a"}', is_public=1, is_shared=1, original_json='{"a": "a"}')) self.session.commit() @@ -1285,9 +1285,9 @@ def test_cascade_on_user_delete(self): On deleting user row, the corresponding row in node table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() @@ -1304,9 +1304,9 @@ def test_cascade_on_graph_delete(self): On deleting graph row, the corresponding row in node table should also be deleted. """ self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() self.session.delete(graph1) @@ -1319,9 +1319,9 @@ def test_cascade_on_graph_delete(self): def test_graph_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() layout1 = self.session.query(Layout).filter(Layout.name == 'layout1').one_or_none() @@ -1330,9 +1330,9 @@ def test_graph_relationship(self): def test_owner_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() @@ -1342,9 +1342,9 @@ def test_owner_relationship(self): def test_default_layout_graph_relationship(self): self.session.add(User(email='owner@example.com', password="password", is_admin=0)) - self.session.add(Graph(name='graph1', owner_email='owner@example.com', json='{}', is_public=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() - self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', json='{}', is_public=0, is_shared=0, original_json='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) self.session.commit() layout1 = self.session.query(Layout).filter(Layout.name == 'layout1').one_or_none() @@ -1356,12 +1356,232 @@ def test_default_layout_graph_relationship(self): self.assertEqual(layout1.default_layout_graph.id, graph1.id) +class GraphVersionModelTestCase(TestCase): + def setUp(self): + db = Database() + # connect to the database + self.connection = db.engine.connect() + # begin a non-ORM transaction + self.trans = self.connection.begin() + # bind an individual Session to the connection + self.session = Session(bind=self.connection) + + def tearDown(self): + self.session.close() + + # rollback - everything that happened with the + # Session above (including calls to commit()) + # is rolled back. + self.trans.rollback() + + # return connection to the Engine + self.connection.close() + + def test_crud_operation(self): + """ + Basic CRUD (Create, Retrieve, Update, Delete) operation should work properly. + """ + + # Create + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.assertEqual(graph1.name, 'graph1') + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.assertEqual(graphversion1.name, 'graphversion1') + # + # Update + graphversion1.name = 'updated_graph_version' + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.assertEqual(graphversion1.name, 'updated_graph_version') + # + # Delete + self.session.delete(graphversion1) + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.assertIsNone(graphversion1) + + # Retrieve + num_graph_versions = self.session.query(GraphVersion).count() + self.assertEqual(num_graph_versions, 0) + + def test_owner_email_fkey_constraint(self): + + with self.assertRaises(IntegrityError): + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=0, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + + def test_graph_id_fkey_constraint(self): + + with self.assertRaises(IntegrityError): + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=0, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + + def test_cascade_on_user_delete(self): + """ + On deleting user row, the corresponding row in graph version table should also be deleted. + """ + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + + owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() + self.session.delete(owner) + self.session.commit() + self.assertIsNone(self.session.query(User).filter(User.email == 'owner@example.com').one_or_none()) + + self.assertIsNone(self.session.query(GraphVersion).filter(and_(GraphVersion.owner_email == 'owner@example.com', GraphVersion.name == 'graphversion1')).one_or_none()) + self.session.commit() + + def test_cascade_on_user_update(self): + """ + On deleting user row, the corresponding row in graph version table should also be updated + """ + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(and_(GraphVersion.owner_email == 'owner@example.com', GraphVersion.name == 'graphversion1')).one_or_none() + + owner = self.session.query(User).filter(User.email == 'owner@example.com').one_or_none() + owner.email = 'owner_updated@example.com' + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(GraphVersion.id == graphversion1.id).one_or_none() + self.assertEqual(graphversion1.owner_email, 'owner_updated@example.com') + self.session.commit() + def test_cascade_on_graph_delete(self): + """ + On deleting graph row, the corresponding row in graph version table should also be deleted. + """ + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + + self.session.delete(graph1) + self.session.commit() + self.assertIsNone(self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none()) + + self.assertIsNone(self.session.query(GraphVersion).filter(and_(GraphVersion.owner_email == 'owner@example.com', GraphVersion.name == 'graphversion1')).one_or_none()) + self.session.commit() + + def test_cascade_on_graph_update(self): + """ + On deleting graph row, the corresponding row in graph version table should also be updated + """ + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(and_(GraphVersion.owner_email == 'owner@example.com', GraphVersion.name == 'graphversion1')).one_or_none() + graph1.id = 10 + self.session.commit() + graphversion1 = self.session.query(GraphVersion).filter(GraphVersion.id == graphversion1.id).one_or_none() + self.assertEqual(graphversion1.graph_id, graph1.id) + self.session.commit() + def test_graph_relationship(self): + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.commit() + graph1 = self.session.query(Graph).filter(and_(Graph.owner_email == 'owner@example.com', Graph.name == 'graph1')).one_or_none() + graphversion1 = self.session.query(GraphVersion).filter(and_(GraphVersion.owner_email == 'owner@example.com', GraphVersion.name == 'graphversion1')).one_or_none() + self.assertEqual(graphversion1.graph.id, graph1.id) +class LayoutToGraphVersionModelTestCase(TestCase): + def setUp(self): + db = Database() + # connect to the database + self.connection = db.engine.connect() + # begin a non-ORM transaction + self.trans = self.connection.begin() + # bind an individual Session to the connection + self.session = Session(bind=self.connection) + def tearDown(self): + self.session.close() + + # rollback - everything that happened with the + # Session above (including calls to commit()) + # is rolled back. + self.trans.rollback() + + # return connection to the Engine + self.connection.close() + + def test_crud_operation(self): + """ + Basic CRUD (Create, Retrieve, Update, Delete) operation should work properly. + """ + + # Create + self.session.add(User(email='owner@example.com', password="password", is_admin=0)) + self.session.add(Graph(name='graph1', owner_email='owner@example.com', is_public=0)) + self.session.commit() + graph1 = self.session.query(Graph).filter(Graph.owner_email == 'owner@example.com').one_or_none() + self.assertEqual(graph1.name, 'graph1') + self.session.add(GraphVersion(name='graphversion1', owner_email='owner@example.com', graph_id=graph1.id, + graph_json='{}', style_json='{}', description='{}')) + self.session.add(Layout(graph_id=graph1.id, name='layout1', owner_email='owner@example.com', is_shared=0, original_json='{}', positions_json='{}', style_json='{}')) + self.session.commit() + layout1 = self.session.query(Layout).filter(Layout.owner_email == 'owner@example.com').one_or_none() + graphversion1 = self.session.query(GraphVersion).filter(GraphVersion.owner_email == 'owner@example.com').one_or_none() + self.session.add(LayoutToGraphVersion(layout_id=layout1.id, graph_version_id=graphversion1.id, status='True')) + self.session.commit() + layouttographversion1 = self.session.query(LayoutToGraphVersion).filter(LayoutToGraphVersion.layout_id == layout1.id).one_or_none() + self.assertEqual(graphversion1.id, layouttographversion1.graph_version_id) + self.assertEqual(layout1.id, layouttographversion1.layout_id) + # + # Update + layouttographversion1.status = 'False' + self.session.commit() + layouttographversion1 = self.session.query(LayoutToGraphVersion).filter(LayoutToGraphVersion.layout_id == layout1.id).one_or_none() + self.assertEqual(layouttographversion1.status, 'False') + # + # Delete + self.session.delete(layouttographversion1) + self.session.commit() + layouttographversion1 = self.session.query(LayoutToGraphVersion).filter(LayoutToGraphVersion.layout_id == layout1.id).one_or_none() + self.assertIsNone(layouttographversion1) + + # Retrieve + num_layout_to_graph_versions = self.session.query(LayoutToGraphVersion).count() + self.assertEqual(num_layout_to_graph_versions, 0) + + + def test_fkey_constraint(self): + + with self.assertRaises(IntegrityError): + self.session.add(GraphVersion(graph_version_id=0, status = 'Null')) + self.session.commit() diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 297f92ab..66798dea 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -8,6 +8,7 @@ url(r'^graphs/(?P[^/]+)$', views.graph_page, name='graph'), url(r'^graphs/(?P[^/]+)/(?P[^/]+)$', views.graph_page_by_name, name='graph_by_name'), url(r'^upload$', views.upload_graph_page, name='upload_graph'), + url(r'^compare$', views.compare_graph_page, name='compare_graph_page'), # AJAX APIs Endpoints @@ -15,6 +16,8 @@ url(r'^ajax/graphs/$', views.graphs_ajax_api, name='graphs_ajax_api'), url(r'^ajax/graphs/advanced_search$', views.graphs_advanced_search_ajax_api, name='graphs_advanced_search_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)$', views.graphs_ajax_api, name='graph_ajax_api'), + url(r'^ajax/compare/$', views.compare_graphs, name='compare_graph'), + url(r'^ajax/compare/multi/$', views.compare_graphs, name='compare_graph'), # Graphs Groups url(r'^ajax/graphs/(?P[^/]+)/groups$', views.graph_groups_ajax_api, name='graph_groups_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)/groups/(?P[^/]+)$', views.graph_groups_ajax_api, name='graph_group_ajax_api'), @@ -27,6 +30,11 @@ # Graph Layouts url(r'^ajax/graphs/(?P[^/]+)/layouts/$', views.graph_layouts_ajax_api, name='graph_layouts_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)/layouts/(?P[^/]+)$', views.graph_layouts_ajax_api, name='graph_layouts_ajax_api'), + # Graph Versions + url(r'^ajax/graphs/(?P[^/]+)/version/$', views.graph_versions_ajax_api, name='graph_versions_ajax_api'), + url(r'^ajax/graphs/(?P[^/]+)/version/(?P[^/]+)$', views.graph_versions_ajax_api, name='graph_versions_ajax_api'), + url(r'^ajax/graphs/(?P[^/]+)/version/(?P[^/]+)/compatibility$', views.graph_versions_to_layout_ajax_api, name='graph_versions_to_layout_ajax_api'), + url(r'^ajax/graphs/(?P[^/]+)/version/(?P[^/]+)/compatibility/(?P[^/]+)$', views.graph_versions_to_layout_ajax_api, name='graph_versions_to_layout_ajax_api'), # REST APIs Endpoints @@ -45,5 +53,8 @@ # Graph Layouts url(r'^api/v1/graphs/(?P[^/]+)/layouts/$', views.graph_layouts_rest_api, name='graph_layouts_rest_api'), url(r'^api/v1/graphs/(?P[^/]+)/layouts/(?P[^/]+)$', views.graph_layouts_rest_api, name='graph_layouts_rest_api'), + # Graph Nodes + url(r'^api/v1/graphs/(?P[^/]+)/version/$', views.graph_versions_rest_api, name='graph_versions_rest_api'), + url(r'^api/v1/graphs/(?P[^/]+)/version/(?P[^/]+)$', views.graph_versions_rest_api, name='graph_versions_rest_api'), ] diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 772639c5..adb112e6 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -14,6 +14,174 @@ from graphspace.wrappers import is_authenticated +def compare_graph_page(request): + """ + Wrapper view function for the following pages: + /compare/ + + Parameters + ---------- + request : HTTP Request + + Returns + ------- + response : HTML Page Response + Rendered graphs list page in HTML. + + Raises + ------ + MethodNotAllowed: If a user tries to send requests other than GET i.e., POST, PUT or UPDATE. + + Notes + ------ + """ + context = RequestContext(request, {}) + if request.GET.get('graph_1') is None and request.GET.get('operation') is None: + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + + if request.GET.getlist('id'): + if request.GET.get('operation') == 'intersection' or request.GET.get('operation') == 'difference': + context['graph_ids'] = json.dumps(request.GET.getlist('id')) + context['operation'] = json.dumps(request.GET.get('operation')) + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + else: + raise MethodNotAllowed(request) + + if request.GET.get('graph_1') and request.GET.get('graph_2') \ + and request.GET.get('operation'): + if request.GET.get('operation') == 'intersection' or request.GET.get('operation') == 'difference': + context['graph_1_id'] = json.dumps(request.GET.get('graph_1')) + context['graph_2_id'] = json.dumps(request.GET.get('graph_2')) + context['operation'] = json.dumps(request.GET.get('operation')) + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + else: + raise MethodNotAllowed(request) + + if request.GET.get('graph') and request.GET.getlist('version') \ + and request.GET.get('operation'): + if request.GET.get('operation') == 'intersection' or request.GET.get('operation') == 'difference': + context['graph_1_id'] = json.dumps(request.GET.get('graph')) + context['version'] = json.dumps(request.GET.getlist('version')) + context['operation'] = json.dumps(request.GET.get('operation')) + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + else: + raise MethodNotAllowed(request) + + +def compare_graphs(request): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + + Returns + ------- + JSON Response of nodes & edges + + Raises + ------ + MethodNotAllowed: If a user tries to send requests other than GET i.e., POST, PUT or UPDATE. + + Notes + ------ + + """ + context = RequestContext(request, {}) + + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if request.method == "GET": + if request.GET.get('multiple'): + return HttpResponse(json.dumps(_compare_graph_multiple(request, request.GET)), + content_type="application/json", status=200) + if request.GET.get('version[]'): + return HttpResponse(json.dumps(_compare_graph_multiple(request, request.GET)), + content_type="application/json", status=200) + + return HttpResponse(json.dumps(_compare_graph(request, request.GET['graph_1_id'], + request.GET['graph_2_id'], request.GET['operation'])), + content_type="application/json", status=200) + else: + raise MethodNotAllowed(request) # Handle other type of request methods like OPTIONS etc. + else: + raise MethodNotAllowed(request) + + +def _compare_graph_multiple(request, data): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + data : dict + graph_ids and operation. Required. + + Returns + ------- + nodes & edges: object + + Raises + ------ + + Notes + ------ + + """ + + # authorization.validate(request, permission='GRAPH_READ', graph_id=graph_1_id) + # authorization.validate(request, permission='GRAPH_READ', graph_id=graph_2_id)` + + # Populate list of graph_ids + id_list = [] + for i in range(1, len(data)-1): + if data.get('graph_id_' + str(i)): + id_list.append(data.get('graph_id_' + str(i))) + + nodes, edges = graphs.get_graph_comparison_multi(request, id_list, data.get('operation')) + edges = [utils.serializer(edge) for edge in edges[1]] + nodes = [utils.serializer(node) for node in nodes[1]] + return {'edges': edges, 'nodes': nodes} + + +def _compare_graph(request, graph_1_id, graph_2_id, operation): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + graph_1_id : string + Unique ID of the 1st graph. Required. + graph_2_id : string + Unique ID of the 2nd graph. Required. + operation : string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_1_id) + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_2_id) + nodes, edges = graphs.get_graph_comparison(request, graph_1_id, graph_2_id, operation) + if operation == 'intersection': + edges = [[utils.serializer(edge) for edge in item] for item in edges[1]] + nodes = [[utils.serializer(node) for node in item] for item in nodes[1]] + else: + edges = [utils.serializer(edge) for edge in edges[1]] + nodes = [utils.serializer(node) for node in nodes[1]] + return {'edges': edges, 'nodes': nodes} + + def upload_graph_page(request): context = RequestContext(request, {}) @@ -113,7 +281,9 @@ def graph_page(request, graph_id): return redirect(request.get_full_path() + '&user_layout=' + context["default_layout_id"]) else: return redirect(request.get_full_path() + '?user_layout=' + context["default_layout_id"]) - + if context['graph']['style_json'] is None: + context['graph']['style_json'] = {} + context['default_version_id'] = context['graph']['default_version_id'] context['graph_json_string'] = json.dumps(context['graph']['graph_json']) context['data'] = {k: json.dumps(v, encoding='ascii') for k,v in context['graph']['graph_json']['data'].items()} context['style_json_string'] = json.dumps(context['graph']['style_json']) @@ -502,6 +672,7 @@ def _update_graph(request, graph_id, graph={}): style_json=graph.get('style_json', None), owner_email=graph.get('owner_email', None) if user_role == authorization.UserRole.ADMIN else None, + default_version_id=graph.get('default_version_id', None), default_layout_id=graph.get('default_layout_id', None))) @@ -1522,3 +1693,443 @@ def _delete_edge(request, graph_id, edge_id): authorization.validate(request, permission='GRAPH_UPDATE', graph_id=graph_id) graphs.delete_edge_by_id(request, edge_id) + + +''' +Graph Version APIs +''' + +@csrf_exempt +@is_authenticated() +def graph_versions_rest_api(request, graph_id, version_id=None): + """ + Handles any request sent to following urls: + /api/v1/graphs//version + /api/v1/graphs//version/ + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + """ + return _graph_versions_api(request, graph_id, version_id=version_id) + + +def graph_versions_ajax_api(request, graph_id, version_id=None): + """ + Handles any request sent to following urls: + /javascript/graphs//version + /javascript/graphs//version/ + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + """ + return _graph_versions_api(request, graph_id, version_id=version_id) + + +def _graph_versions_api(request, graph_id, version_id=None): + """ + Handles any request (GET/POST) sent to version/ or version/. + + Parameters + ---------- + request - HTTP Request + graph_id : string + Unique ID of the graph. + version_id : string + Unique ID of the version. + + Returns + ------- + + """ + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if request.method == "GET" and version_id is None: + return HttpResponse(json.dumps(_get_graph_versions(request, graph_id, query=request.GET)), + content_type="application/json") + elif request.method == "GET" and version_id is not None: + return HttpResponse(json.dumps(_get_graph_version(request, graph_id, version_id)), + content_type="application/json") + elif request.method == "POST" and version_id is None: + return HttpResponse(json.dumps(_add_graph_version(request, graph_id, graph_version=json.loads(request.body))), + content_type="application/json", + status=201) + elif request.method == "DELETE" and version_id is not None: + _delete_graph_version(request, graph_id, version_id) + return HttpResponse(json.dumps({ + "message": "Successfully deleted Graph Version with id=%s" % (version_id) + }), content_type="application/json", status=200) + else: + raise MethodNotAllowed(request) # Handle other type of request methods like OPTIONS etc. + else: + raise BadRequest(request) + + +def _get_graph_versions(request, graph_id, query=dict()): + """ + + Query Parameters + ---------- + graph_id : string + Unique ID of the graph. + limit : integer + Number of entities to return. Default value is 20. + offset : integer + Offset the list of returned entities by this number. Default value is 0. + names : list of strings + Search for versions with given names. In order to search for versions with given name as a substring, wrap the name with percentage symbol. For example, %xyz% will search for all versions with xyz in their name. + order : string + Defines the column sort order, can only be 'asc' or 'desc'. + sort : string + Defines which column will be sorted. + + + Parameters + ---------- + request : object + HTTP GET Request. + graph_id : string + Unique ID of the graph. + + Returns + ------- + total : integer + Number of graph versions matching the request. + versions : List of versions. + List of Version Objects with given limit and offset. + + Raises + ------ + BadRequest - `User is not authorized to access private graphs created by given owner. This means either the graph belongs to a different owner + or graph is not shared with the user. + + Notes + ------ + + """ + + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) + + querydict = QueryDict('', mutable=True) + querydict.update(query) + query = querydict + + # Validate search graphs API request + user_role = authorization.user_role(request) + if user_role == authorization.UserRole.LOGGED_IN: + if query.get('is_public', None) != '1': + if get_request_user(request) != query.get('member_email', None) \ + and get_request_user(request) != query.get('owner_email', None): + raise BadRequest(request, error_code=ErrorCodes.Validation.NotAllowedGraphAccess, + args=query.get('owner_email', None)) + + total, versions_list = graphs.search_graph_versions(request, + graph_id=graph_id, + names=query.getlist('names[]', None), + limit=query.get('limit', 20), + offset=query.get('offset', 0), + order=query.get('order', 'desc'), + sort=query.get('sort', 'name')) + + return { + 'total': total, + 'versions': [utils.serializer(version, summary=True) for version in versions_list] + } + + +def _get_graph_version(request, graph_id, version_id): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + version_id : string + Unique ID of the version. + + Returns + ------- + version: object + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) + return utils.serializer(graphs.get_graph_version_by_id(request, version_id)) + + +@is_authenticated() +def _add_graph_version(request, graph_id, graph_version={}): + """ + Node Parameters + ---------- + name : string + Name of the node. Required + owner_email : string + Email of the Owner of the graph. Required + graph_id : string + Unique ID of the graph. Required + + + Parameters + ---------- + graph_json : dict + Dictionary containing the graph_json data of the graph being added. + request : object + HTTP POST Request. + + Returns + ------- + graph_version : object + Newly created graph_version object. + + Raises + ------ + + Notes + ------ + + """ + + + return utils.serializer(graphs.add_graph_version(request, + name=graph_version.get('name', None), + description=graph_version.get('description', None), + owner_email=graph_version.get('owner_email', None), + graph_json=graph_version.get('graph_json', None), + style_json=graph_version.get('style_json', None), + graph_id=graph_id)) + + +@is_authenticated() +def _delete_graph_version(request, graph_id, graph_version_id): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + graph_version_id : string + Unique ID of the Graph Version. + + Returns + ------- + None + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_UPDATE', graph_id=graph_id) + graphs.delete_graph_version_by_id(request, graph_version_id) + + +@csrf_exempt +@is_authenticated() +def graph_versions_to_layout_ajax_api(request, graph_id, version_id, layout_id=None): + """ + Handles any request sent to following urls: + /javascript/graphs//version//layouts/ + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + """ + return _graph_versions_to_layout_api(request, graph_id=graph_id, graph_version_id=version_id, layout_id=layout_id) + + +def _graph_versions_to_layout_api(request, graph_id, graph_version_id, layout_id=None): + """ + Handles any request (GET/POST) sent to version//layout/ + + Parameters + ---------- + request - HTTP Request + graph_id : string + Unique ID of the graph. + version_id : string + Unique ID of the version. + layout_id : string + Unique ID of the layout. + + Returns + ------- + + """ + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if request.method == "GET" and graph_version_id is not None and layout_id is not None: + return HttpResponse(json.dumps(_get_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id)), + content_type="application/json") + elif request.method == "POST" and graph_version_id is not None and layout_id is not None: + return HttpResponse(json.dumps(_add_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id, status=json.loads(request.body)['compatibility_status'])), + content_type="application/json", + status=201) + elif request.method == "PUT" and graph_version_id is not None: + return HttpResponse(json.dumps(_update_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id, status=json.loads(request.body)['compatibility_status'])), + content_type="application/json", + status=200) + elif request.method == "DELETE" and graph_version_id is not None and layout_id is not None: + _delete_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id) + return HttpResponse(json.dumps({ + "message": "Successfully deleted Graph Version with id=%s's compatibility status with Layout with id =%s" % (graph_version_id, layout_id) + }), content_type="application/json", status=200) + else: + raise MethodNotAllowed(request) # Handle other type of request methods like OPTIONS etc. + else: + raise BadRequest(request) + + +@is_authenticated() +def _add_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id, status=None): + """ + Parameters + ---------- + request : object + HTTP POST Request. + graph_id : string + Unique ID of the graph. Required + graph_version_id : string + Unique ID of the graph version. Required + layout_id : string + Unique ID of the layout. Required + + + Returns + ------- + layout_to_graph_version : object + Newly created layout_to_graph_version object. + + Raises + ------ + + Notes + ------ + + """ + + return utils.serializer(graphs.add_graph_version_to_layout_status(request, + graph_version_id=graph_version_id, + layout_id=layout_id, + status=status)) + + +def _get_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id): + """ + + Parameters + ---------- + request : object + HTTP GET Request + graph_id : string + Unique ID of the graph. Required. + graph_version_id : string + Unique ID of the Graph Version + layout_id : string + Unique ID of the Layout. + + Returns + ------- + version: object + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) + return utils.serializer(graphs.get_graph_version_to_layout_status(request, + graph_version_id=graph_version_id, + layout_id=layout_id)) + + +@is_authenticated() +def _update_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id=None, status=None): + """ + Parameters + ---------- + request : object + HTTP GET Request. + graph_id : string + Unique ID of the graph. Required + graph_version_id : string + Unique ID of the Graph Version + layout_id : string + Unique ID of the Layout. + status : Boolean + Compatibility Status. [Default : None] + + Returns + ------- + layout_to_graph_version : object + Updated LayoutToGraphVersion object. + + Raises + ------ + + Notes + ------ + + If no layout_id is passed then all the Layouts associated with the Graph Version is updated. + If no status is passed then status is set to None by default. + + """ + authorization.validate(request, permission='GRAPH_UPDATE', graph_id=graph_id) + + return utils.serializer(graphs.update_graph_version_to_layout_status(request, + graph_version_id=graph_version_id, + layout_id=layout_id, + status=status)) + + +@is_authenticated() +def _delete_graph_version_to_layout_status(request, graph_id, graph_version_id, layout_id): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + graph_id : string + Unique ID of the graph. Required + graph_version_id : string + Unique ID of the Graph Version + layout_id : string + Unique ID of the Layout. + + Returns + ------- + None + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_UPDATE', graph_id=graph_id) + graphs.delete_graph_version_to_layout_status(request, graph_version_id, layout_id) diff --git a/applications/home/views.py b/applications/home/views.py index d9ddf1a0..a70eeb92 100644 --- a/applications/home/views.py +++ b/applications/home/views.py @@ -160,9 +160,9 @@ def forgot_password_page(request): if password_reset_code is not None: users.send_password_reset_email(request, password_reset_code) - context["success_message"] = "Email has been sent!" + context["success_message"] = "You will receive an email with link to update the password!" else: - context["error_message"] = "Email does not exist!" + context["error_message"] = "You will receive an email with link to update the password!" return render(request, 'forgot_password/index.html', context) # Handle POST request to forgot password page. else: raise MethodNotAllowed(request) # Handle other type of request methods like PUT, UPDATE. diff --git a/bower.json b/bower.json index 7b986c01..37f1dbbc 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,7 @@ "moment": "^2.17.0", "remarkable-bootstrap-notify": "^3.1.3", "animate.css": "^3.5.2", - "cytoscape": "^2.7.11", + "cytoscape": "^3.2.17", "webcola": "^3.3.0", "bootstrap": "^3.3.7", "cytoscape-cola": "^1.6.0", @@ -30,7 +30,7 @@ "bootstrap-table": "^1.11.0", "cytoscape-panzoom": "^2.4.0", "select2": "select2-dist#^4.0.3", - "cytoscape-context-menus": "^2.1.1", + "cytoscape-context-menus": "^3.0.6", "bootstrap-colorpicker": "^2.5.1" } } diff --git a/docs/Programmers_Guide.md b/docs/Programmers_Guide.md index f2434b9d..ee610fbc 100644 --- a/docs/Programmers_Guide.md +++ b/docs/Programmers_Guide.md @@ -50,3 +50,57 @@ Install graphspace_python from PyPI using: ### Usage Please refer to ``graphspace_python`` package's [documentation](http://manual.graphspace.org/projects/graphspace-python/) to learn how to use it. + + +## GraphSpace REST APIs using the Postman app + +### This documentation is based on [Sandeep Mahapatra's blog post](https://summerofcode17.wordpress.com/2017/05/30/using-the-graphspace-restful-api/) in the 2017 GSoc. + + +``` + Note: In order to fully utilize the features of GraphSpace REST API, you must have an account on GraphSpace. +``` + +Postman is a Google Chrome app for interacting with HTTP APIs. It provides a friendly GUI for constructing requests and reading responses. Postman makes it easy to test, develop and document APIs by allowing users to quickly put together both simple and complex HTTP requests. + +### Postman Installation + +Postman is available as a [native app](https://www.getpostman.com/docs/install_native) (recommended) for Mac / Windows / Linux, and as a Chrome App. The Postman Chrome app can only run on the Chrome browser. To use the Postman Chrome app, you need to: +- Install Google Chrome: [Install Chrome](https://www.google.com/chrome/). +- If you already have Chrome installed, head over to Postman’s page on the [Chrome Webstore](https://chrome.google.com/webstore/detail/postman-rest-client-packa/fhbjgbiflinjbdggehcddcbncdddomop?hl=en), and click ‘Add to Chrome’. +- After the download is complete, launch the app. + +### Using Postman for GraphSpace REST API + +The GraphSpace REST APIs have the base URL http://www.graphspace.org/api/v1/. There are many endpoints defined under this base URL (the documentation of which can be found here), but to learn and understand the usage of GraphSpace REST APIs through Postman, we would be considering only the /graphs endpoint for GET and POST request. +- The GET /graphs request fetches a list of graphs from GraphSpace matching the query parameters. +- The POST /graphs request creates a graph in GraphSpace. + +### GET /graphs +- The URL is the first thing that we would be setting for a request. We will set the URL to http://www.graphspace.org/api/v1/graphs. +![Rest API get](_static/images/rest-api/gs_rest_get_1.jpg) +- Provide Authorization: Select ‘Basic Auth’ from Authorization type drop-down. Enter the username and password and click on ‘Update Request’. +![Rest API get 2](_static/images/rest-api/gs_rest_get_2.jpg) +- Set Header: Add the following key value pairs, ```Content-Type:application/json and Accept:application/json.``` +![REST API get 3](_static/images/rest-api/gs_rest_get_3.jpg) +- Select Method: Changing the method is straightforward. Just select the method from the select control. We will use GET method here. +- Add URL Params: Clicking on the URL Params button will open up the key-value editor for entering URL parameters. The details of the URL Params for /graphs endpoint can be found in the [documentation](http://manual.graphspace.org/en/latest/Programmers_Guide.html#api-reference). +- Click on the Send button to the send the request. A list of graphs matching the query parameters will be received in the response. + +### POST /graphs +- The initial steps of setting URL, Authorization and Header are performed. +- Change Method to POST. +- Set Request Body: Click on Body to open the request body editor. Select raw request from the choices and JSON(application/json) from the drop-down. Enter the json data for the graph to be created in the editor. The details regarding the properties of the json graph body can be found in the [documentation](http://manual.graphspace.org/en/latest/Programmers_Guide.html#api-reference). +![REST API post 1](_static/images/rest-api/gs_rest_post_1.jpg) +- Click on the Send button to the send the request. A new graph object will be created and returned in the response. + +### Postman Collection + +A collection lets you group individual requests together. These requests can be further organized into folders to accurately mirror our API. Requests can also store sample responses when saved in a collection. You can add metadata like name and description too so that all the information that a developer needs to use your API is available easily. Collections can be exported as JSON files. Exporting a collection also saves the Authorization details. Hence, it is advised to remove the Authorization details from the Header before exporting. + +For quick use of the GraphSpace REST APIs or if you are stuck somewhere and you want reference, you can [download the collection of the APIs here](https://gist.github.com/sandeepm96/a824a6d0e643811389a6bf212e30a381). The collection has details regarding the API endpoints like params and body properties. Importing steps: +- Click Import button in the top menu. +- Choose the Import File in the pop up window. +![post man collection](_static/images/rest-api/post_man_collection.jpg) +- Provide the Authorization details for the imported requests (as Authorization details have been removed for security concern) + diff --git a/docs/_static/images/rest-api/gs_rest_get_1.jpg b/docs/_static/images/rest-api/gs_rest_get_1.jpg new file mode 100644 index 00000000..c0c56450 Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_1.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_get_2.jpg b/docs/_static/images/rest-api/gs_rest_get_2.jpg new file mode 100644 index 00000000..20e4355d Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_2.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_get_3.jpg b/docs/_static/images/rest-api/gs_rest_get_3.jpg new file mode 100644 index 00000000..fc2c7d2a Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_3.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_post_1.jpg b/docs/_static/images/rest-api/gs_rest_post_1.jpg new file mode 100644 index 00000000..cecbea69 Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_post_1.jpg differ diff --git a/docs/_static/images/rest-api/post_man_collection.jpg b/docs/_static/images/rest-api/post_man_collection.jpg new file mode 100644 index 00000000..9e235817 Binary files /dev/null and b/docs/_static/images/rest-api/post_man_collection.jpg differ diff --git a/graphspace/database.py b/graphspace/database.py index b750a367..f92025bf 100644 --- a/graphspace/database.py +++ b/graphspace/database.py @@ -20,6 +20,11 @@ def __init__(self): self.engine = create_engine(''.join( ['postgresql://', config['USER'], ':', config['PASSWORD'], '@', config['HOST'], ':', config['PORT'], '/', config['NAME']]), echo=False) # TODO: Find out what is the use of metadata and reflection. + self.connection = self.engine.connect() + result = self.connection.execute("SELECT * FROM pg_extension where extname like 'pg_trgm'") + if result.rowcount==0: + self.connection.execute("create extension btree_gin") + self.connection.execute("create extension pg_trgm") settings.BASE.metadata.create_all(self.engine) self.meta = sqlalchemy.schema.MetaData() self.meta.reflect(bind=self.engine) diff --git a/graphspace/settings/local.py b/graphspace/settings/local.py index b15da20d..ac0a5f16 100644 --- a/graphspace/settings/local.py +++ b/graphspace/settings/local.py @@ -40,9 +40,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'test_database', - 'USER': 'adb', - 'PASSWORD': '', + 'NAME': 'graphspace', + 'USER': 'postgres', + 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '5432' } diff --git a/migration/versions/09f74ae3229b_add_table_layout_compatibility.py b/migration/versions/09f74ae3229b_add_table_layout_compatibility.py new file mode 100644 index 00000000..0b3f6579 --- /dev/null +++ b/migration/versions/09f74ae3229b_add_table_layout_compatibility.py @@ -0,0 +1,37 @@ +"""add_table_layout_to_graph_version + +Revision ID: 09f74ae3229b +Revises: bb9a45e2ee5e +Create Date: 2018-06-24 17:30:43.566000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '09f74ae3229b' +down_revision = 'bb9a45e2ee5e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'layout_to_graph_version', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('layout_id', sa.String, nullable=False, unique=True), + sa.Column('graph_version_id', sa.Integer, nullable=False), + sa.Column('status', sa.String, nullable=True), + ) + # Add date columns + op.add_column('layout_to_graph_version', sa.Column('created_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + op.add_column('layout_to_graph_version', sa.Column('updated_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + + # Add Foreign Keys + op.execute('ALTER TABLE layout_to_graph_version ADD CONSTRAINT layout_to_graph_version_layout_id_fkey FOREIGN KEY (layout_id) REFERENCES "layout" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + op.execute('ALTER TABLE layout_to_graph_version ADD CONSTRAINT layout_to_graph_version_graph_version_id_fkey FOREIGN KEY (graph_version_id) REFERENCES "graph_version" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + + +def downgrade(): + op.drop_table('layout_to_graph_version') diff --git a/migration/versions/840db85c5bce_update_graph_table_migrate_json_to_.py b/migration/versions/840db85c5bce_update_graph_table_migrate_json_to_.py new file mode 100644 index 00000000..4adf545e --- /dev/null +++ b/migration/versions/840db85c5bce_update_graph_table_migrate_json_to_.py @@ -0,0 +1,41 @@ +"""update_graph_table_migrate_json_to_graph_version + +Revision ID: 840db85c5bce +Revises: f8f6ba9712df +Create Date: 2018-06-23 02:27:29.434000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '840db85c5bce' +down_revision = 'f8f6ba9712df' +branch_labels = None +depends_on = None + + +def upgrade(): + + # Drop columns which have been migrated to graph_version table + op.drop_column('graph', 'style_json') + op.drop_column('graph', 'graph_json') + + # Add new column for default_graph_version_id + op.add_column('graph', sa.Column('default_version_id', sa.Integer)) + + # Add new foreign key reference + op.execute('ALTER TABLE graph ADD CONSTRAINT graph_default_version_id_fkey FOREIGN KEY (default_version_id) REFERENCES "graph_version" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + +def downgrade(): + + # Add columns which have been migrated to graph_version table + op.add_column('graph', sa.Column('style_json', sa.String)) + op.add_column('graph', sa.Column('graph_json', sa.String)) + + # Remove foreign key reference + op.drop_constraint('graph_default_version_id_fkey', 'graph', type_='foreignkey') + + # Drop column for default_graph_version_id + op.drop_column('graph', 'default_version_id') diff --git a/migration/versions/f8f6ba9712df_create_graph_versions_table.py b/migration/versions/f8f6ba9712df_create_graph_versions_table.py new file mode 100644 index 00000000..e5cd5005 --- /dev/null +++ b/migration/versions/f8f6ba9712df_create_graph_versions_table.py @@ -0,0 +1,42 @@ +"""create_graph_versions_table + +Revision ID: f8f6ba9712df +Revises: bb9a45e2ee5e +Create Date: 2018-06-22 23:08:39.459000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f8f6ba9712df' +down_revision = 'bb9a45e2ee5e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'graph_version', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('name', sa.String, nullable=False, unique=True), + sa.Column('graph_id', sa.Integer, nullable=False), + sa.Column('owner_email', sa.String, nullable=False), + sa.Column('graph_json', sa.String, nullable=False), + sa.Column('style_json', sa.String, nullable=False), + sa.Column('description', sa.String, nullable=False), + ) + op.add_column('graph_version', sa.Column('created_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + op.add_column('graph_version', sa.Column('updated_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + # Create New Index + op.create_index('graph_version_idx_name', 'graph_version', ['name'], unique=True) + # Add new foreign key reference + op.execute('ALTER TABLE graph_version ADD CONSTRAINT graph_version_graph_id_fkey FOREIGN KEY (graph_id) REFERENCES "graph" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + op.execute('ALTER TABLE graph_version ADD CONSTRAINT graph_version_owner_email_fkey FOREIGN KEY (owner_email) REFERENCES "user" (email) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + + + + +def downgrade(): + op.drop_table('graph_version') diff --git a/requirements.txt b/requirements.txt index 1f6c5be8..67c5bd1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ MarkupSafe==0.23 networkx==1.11 oauthlib==1.1.2 poster==0.8.1 -psycopg2==2.6.2 +psycopg2 py-bcrypt==0.4 Pygments==2.1.3 pytz==2016.4 diff --git a/static/css/graphspace.css b/static/css/graphspace.css index ff68bde3..4c35b68f 100644 --- a/static/css/graphspace.css +++ b/static/css/graphspace.css @@ -261,6 +261,22 @@ ul.nav.nav-tabs > li > a:hover { border-left: 5px double rgba(36, 41, 46, 0.21); } +#compare-sidebar { + z-index: 1000; + position: absolute; + right: 250px; + width: 250px; + height: 100%; + margin-right: -250px; + overflow-y: scroll; + background: transparent; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; + border-left: 5px double rgba(36, 41, 46, 0.21); +} + #wrapper.toggled #sidebar-wrapper { width: 250px; } @@ -438,4 +454,35 @@ p.lead { position: relative; margin-left: 0; } +} + +.compare-table-td { + margin-right:15px; +} + +#search-place-holder { + padding: 20px; + margin-top: -6px; + width: 100%; + border: 0; + border-radius: 0; + background: #f1f1f1; +} + +#parameter-header { + text-align: left; + padding: 20px 30px 25px 20px; + border: 1px solid #e1e4e5; + border-radius: 3px; +} + +.graph_version_span { + cursor:pointer; + color:#337ab7; + +} + +.graph_version_span:hover { + text-decoration:underline; + } \ No newline at end of file diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js new file mode 100644 index 00000000..be9088c7 --- /dev/null +++ b/static/js/graphs_compare.js @@ -0,0 +1,1283 @@ +/** + * Created by jahan on 10/07/19. + */ + +var compareGraphPage = { + cyGraph: undefined, + graph_ids: [], + graphs_json: [], + styles_json: [], + common_nodes: undefined, + common_metadata: {}, + edge_name_to_id: {}, + timeout: null, + init: function () { + /** + * This function is called to setup the upload graph page. + * It will initialize all the event listeners. + */ + + $('#nodes-li').hide(); + $('#edges-li').hide(); + $('#visualization-li').hide(); + compareGraphPage.loadGraphs(); + /*$('#graphVisualizationTabBtn').click(function (e) { + window.setTimeout(function () { + $('#cyGraphContainer').css('height', '99%'); + compareGraphPage.cyGraph.fit().center(); + }, 100); + + });*/ + $('#resetMenus').click(function () { + compareGraphPage.resetMenus(1,8); + $('#compareModal').modal('hide'); + }); + $("#search-place-holder").on("keyup", function () { + var value = $(this).val().toLowerCase(); + $(".dropdown-menu li").filter(function () { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + }); + }); + /* + $("#dropdownMenu1").on("change", function(){ + if ($('#dropdownMenu1').attr('value') && $('#dropdownMenu2').attr('value') && $('#operatorMenu1').attr('value')) + compareGraphPage.compareGraphs(); + }); + */ + // $("#dropdownMenu1").on("change", function () { + // var value = ""; + // $("#search-place-holder").val(""); + // $(".dropdown-menu li").filter(function () { + // $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + // }); + // if( $('#dropdownMenu1').val() && $('#dropdownMenu2').val() && $('#operatorMenu1').val()){ + // compareGraphPage.compareGraphs(); + // } + // }); + // $("#dropdownMenu2").on("focusin", function () { + // var value = ""; + // $("#search-place-holder").val(""); + // $(".dropdown-menu li").filter(function () { + // $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + // }); + // }); + $('#colorpicker1').colorpicker(); + $('#colorpicker1').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker1'), event, 'graph_1', $('#operatorcolorpicker')); + }); + $('#colorpicker2').colorpicker(); + $('#colorpicker2').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker2'), event, 'graph_2', $('#operatorcolorpicker')); + }); + $('#colorpicker3').colorpicker(); + $('#colorpicker3').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker3'), event, 'graph_1', $('#operatorcolorpicker1')); + }); + $('#colorpicker4').colorpicker(); + $('#colorpicker4').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker4'), event, 'graph_2', $('#operatorcolorpicker1')); + }); + $('#colorpicker5').colorpicker(); + $('#colorpicker5').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker5'), event, 'graph_3', $('#operatorcolorpicker1')); + }); + $('#colorpicker6').colorpicker(); + $('#colorpicker6').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker6'), event, 'graph_4', $('#operatorcolorpicker1')); + }); + $('#colorpicker7').colorpicker(); + $('#colorpicker7').colorpicker().on('changeColor', function (event) { + compareGraphPage.colorPickerHelper($('#colorpicker7'), event, 'graph_5', $('#operatorcolorpicker1')); + }); + + $('#operatorcolorpicker').colorpicker(); + $('#operatorcolorpicker').colorpicker().on('changeColor', function (event) { + $('#operatorcolorpicker').css('background-color', event.color.toString()); + $('#operatorcolorpicker').val(event.color.toString()); + compareGraphPage.setNodesColor('common_1', event.color.toString()); + }); + $('#operatorcolorpicker1').colorpicker(); + $('#operatorcolorpicker1').colorpicker().on('changeColor', function (event) { + $('#operatorcolorpicker1').css('background-color', event.color.toString()); + $('#operatorcolorpicker1').val(event.color.toString()); + compareGraphPage.setNodesColor('common_1', event.color.toString()); + }); + if (graph_1_id && graph_2_id && operation) { + $('#dropdownMenu1').attr('value', graph_1_id); + $('#dropdownMenu2').attr('value', graph_2_id); + $('#operatorMenu1').attr('value', operation); + $('#operatorMenu1').parent().find('a[row_id="' + operation + '"]').click(); + } + if (graph_ids && operation) { + for (let i = 0; i < graph_ids.length; i++) { + $('#dropdownMenu' + (i + 3)).attr('value', graph_ids[i]); + } + $('#operatorMenu2').attr('value', operation); + $('#operatorMenu2').parent().find('a[row_id="' + operation + '"]').click(); + } + if (version && operation) { + $('#dropdownMenu1').attr('value', graph_1_id); + $('#dropdownMenu2').attr('value', graph_1_id); + $('#operatorMenu1').attr('value', operation); + $('#operatorMenu1').parent().find('a[row_id="' + operation + '"]').click(); + // compareGraphPage.compareGraphVersions(); + } + }, + selectGraphForCompare: function (obj, graph_id) { + /** + * This function is called to when User selects + * graphs for comparison from Graph Index Page. + * It generates the graph comparison url for the selected graphs. + */ + let href = '/compare?' + if (obj[0].checked){ + compareGraphPage.graph_ids.push(graph_id); + } + else { + for( var i = 0; i < compareGraphPage.graph_ids.length; i++){ + if ( compareGraphPage.graph_ids[i] === graph_id) { + compareGraphPage.graph_ids.splice(i, 1); + } + } + } + _.each(compareGraphPage.graph_ids, function (item) { + href += 'id=' + item + '&'; + }); + /* + _.each($('input:checkbox:checked'), function (item) { + href += 'id=' + item.getAttribute('row_id') + '&'; + }); + */ + $('.bootstrap-table > .fixed-table-toolbar').addClass('pull-right'); + if ($('input:checkbox:checked').length > 1) { + console.log('Compare'); + _.each($('[id^=compareHrefDiv] > a[data="Intersection"]'), function(item){ + item.setAttribute('href', href + 'operation=intersection'); + }); + $('[id^=compareHrefDiv]').css('visibility', 'visible'); + } else { + $('[id^=compareHrefDiv]').css('visibility', 'hidden'); + } + if ($('input:checkbox:checked').length == 2) { + console.log('Compare'); + _.each($('[id^=compareHrefDiv] > a[data="Intersection"]'), function(item){ + item.setAttribute('href', '/compare?graph_1=' + compareGraphPage.graph_ids[0] + + '&graph_2=' + compareGraphPage.graph_ids[1] + '&operation=intersection'); + }); + $('[id^=compareHrefDiv]').css('visibility', 'visible'); + } + }, + colorPickerHelper: function (obj, event, graph_id, common) { + /** + * This function is called to whenever a color is selected + * using the color picker. + */ + obj.css('background-color', event.color.toString()); + obj.val(event.color.toString()); + if (compareGraphPage.cyGraph) { + compareGraphPage.setNodesColor(graph_id, event.color.toString()); + compareGraphPage.setNodesColor('common_1', common.val()); + } + }, + setDropdownLabel: function(source, target){ + target.attr('value', source.attr('row_id')); + target.val(source.attr('row_id')); + target.children('i').text(' '); + target.children('bold').text(source.attr('data')); + }, + resetMenus: function (start, end) { + /** + * This function is called to whenever user wants to reset selection. + * All dropdown menus are reset to default state. + * graph_ids need to reset to allow fresh graph comparison + */ + $('#nodes-li').hide(); + $('#visualization-li').hide(); + $('#edges-li').hide(); + $('#cyGraphContainer').hide(); + if (start==1) + compareGraphPage.graph_ids = []; + for (let i = start; i < end; i++) { + $('#dropdownMenu' + i).attr('value', undefined); + $('#dropdownMenu' + i).val(''); + $('#dropdownMenu' + i).children().text(''); + $('#dropdownMenu' + i + ' > i').text('Select Graph ' + i); + if (i < 3) { + $('#operatorMenu' + i).attr('value', undefined); + $('#operatorMenu' + i).text('Select Operation'); + $('#operatorMenu' + i).append(''); + } + } + }, + validateExpression: function (infix) { + /** + * Might be needed in future! + * + */ + var balance = 0; + // remove white spaces to simplify regex + infix = infix.replace(/ /g, ''); + var regex = /[\+\-]?\w+(([\+\-\*\/\&\|\!]|(\<\=?|\>\=?|\=\=|\!=))[\+\-]?\w+)*/; + + // if it has empty parenthesis then is not valid + if (infix.match(/\(\)/)) { + return false; + } + + // validate parenthesis balance + for (var i = 0; i < infix.length; i++) { + if (infix[i] == '(') { + balance++; + } else if (infix[i] == ')') { + balance--; + } + + if (balance < 0) { + return false; + } + } + + if (balance > 0) { + return false; + } + + // remove all the parenthesis + infix = infix.replace(/[\(\)]/g, ''); + + return infix.match(regex)[0] == infix; + }, + loadGraphs: function () { + /** + * This function is called when compare graph page is loaded. + * It sends ajax request to fetch My graphs, Shared Graphs and Public graphs. + */ + var params = {'data': {'sort': 'updated_at', 'order': 'desc', 'offset': 0, 'limit': 10}}; + query = ''; + params.data["owner_email"] = $('#UserEmail').val(); + + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + delete params.data.owner_email; + params.data["member_email"] = $('#UserEmail').val(); + + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + delete params.data.member_email; + + params.data["is_public"] = 1; + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + setDropdownMenu: function (obj) { + /** + * This helper function changes the header values of dropdown menu to + * correspond to the selected value. + * It called in populateCompareDropdownMenu(). + */ + if (obj.parent().parent().siblings('button').attr('id') == "operatorMenu1") + $('#operatorMenu1').attr('value', obj.attr('row_id')); + if (obj.parent().parent().siblings('button').attr('id') == "dropdownMenu1") + { + compareGraphPage.setDropdownLabel(obj, $('#dropdownMenu3')); + $('#dropdownMenu1').attr('value', obj.attr('row_id')) + } + if (obj.parent().parent().siblings('button').attr('id') == "dropdownMenu2") + { + compareGraphPage.setDropdownLabel(obj, $('#dropdownMenu4')); + $('#dropdownMenu2').attr('value', obj.attr('row_id')) + } + if (obj.parent().parent().siblings('button').attr('id') == "dropdownMenu3") + compareGraphPage.setDropdownLabel(obj, $('#dropdownMenu1')); + if (obj.parent().parent().siblings('button').attr('id') == "dropdownMenu4") + compareGraphPage.setDropdownLabel(obj, $('#dropdownMenu2')); + if (obj.parent().parent().siblings('button').attr('id')[12]>2) + $('#operatorMenu1').attr('value', ''); + + var label = obj.parent().parent().siblings('button').children('i'); + if (label.text()) { + // label.text(label.attr('value')); + label.text(' '); + obj.parent().parent().siblings('button').children('bold').text(obj.attr('data')); + } else { + obj.parent().parent().siblings("button[class*='dropdown-toggle']").text(obj.attr('data')); + obj.parent().parent().siblings("button[class*='dropdown-toggle']").append(''); + + // if (obj.parent().parent().siblings("button[class*='dropdown-toggle']")[0].id == 'operatorMenu1') { + // $('#operatorMenu2').attr('value', undefined); + // compareGraphPage.graph_ids.length=2; + // compareGraphPage.compareGraphs(); + // compareGraphPage.resetMenus(3,8) + // // compareGraphPage.compareGraphsMultiple(); + // } + if (obj.parent().parent().siblings("button[class*='dropdown-toggle']")[0].id == 'operatorMenu2') { + $('#operatorMenu1').attr('value', ''); + compareGraphPage.compareGraphsMultiple(); + } + } + if($('#operatorMenu1').attr('value') && $('#dropdownMenu1').attr('value')&& $('#dropdownMenu2').attr('value')){ + $('#operatorMenu2').attr('value', ''); + compareGraphPage.graph_ids.length=2; + compareGraphPage.compareGraphs(); + compareGraphPage.resetMenus(3,8) + } + // if ($('#dropdownMenu1').attr('value') && $('#dropdownMenu2').attr('value') && $('#operatorMenu1').attr('value')) + // compareGraphPage.compareGraphs(); + obj.parent().parent().siblings("button[class*='dropdown-toggle']").attr("value", obj.attr('row_id')); + }, + populateCompareDropdownMenu: function (data) { + /** + * This helper function populates the dropdown menu with graphs available to user. + * It is called in loadGraphs() when compare graph page is loaded. + */ + $.each(data, function (i, item) { + if ($('#UserEmail').val() == item.owner_email && $(".compare-dropdown[mygraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('mygraphs', 'true'); + } else if (item.owner_email.startsWith("public") && $(".compare-dropdown[publicgraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('publicgraphs', 'true'); + } else if ($('#UserEmail').val() != item.owner_email && !item.owner_email.startsWith("public") && $(".compare-dropdown[sharedgraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('sharedgraphs', 'true'); + } + $(".compare-dropdown").append('
  • ' + + item.name + '
  • ') + }); + }, + setNodesColor: function (graph_parent, color) { + /** + * Params + * graph_parent : identifies the group of nodes + * color : color for the group of nodes + * + * This function color a group of nodes according to the class of the element. + * It is called when comparison is executed and whenever user changes node + * color dynamically using the colorpickers. + */ + compareGraphPage.cyGraph.$("." + graph_parent).style({ + 'background-color': color, + 'border-color': color + }); + compareGraphPage.cyGraph.$("." + graph_parent).connectedEdges().style({'line-color': color}); + }, + setNodesColorMultiple: function (graph_parent, color) { + /** + * Params + * graph_parent : identifies the group of nodes + * color : color for the group of nodes + * + * This function color a group of nodes according to the graph_parent id. + * It is called when comparison is executed and whenever user changes node + * color dynamically using the colorpickers. + */ + compareGraphPage.cyGraph.$("." + graph_parent).style({ + 'background-color': color, + 'background-opacity': 0, + 'border-opacity': 0, + 'border-color': color, + }); + compareGraphPage.cyGraph.$("." + graph_parent).style({ + 'background-color': color, + 'border-color': color + }); + compareGraphPage.cyGraph.$("." + graph_parent).connectedEdges().style({'line-color': color}); + + for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + compareGraphPage.cyGraph.$(".graph_" + (i + 1)).style({ + 'background-opacity': 0, + 'font-size': '1px' + }); + } + compareGraphPage.cyGraph.$('.common_1').style({'background-opacity': 0, 'font-size': '1px'}); + }, + setGraph0Groups: function (graph_json, common_nodes) { + /** + * Not used anymore. For historical compatibility and reference. + * Will be removed in final draft + */ + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + graph_json['elements']['nodes'][len] = { + 'data': { + 'id': 'graph_1', + 'label': 'Graph 1', + 'name': 'Graph 1' + } + }; + graph_json['elements']['nodes'][len + 1] = {'data': {'id': 'common_1', 'label': 'Common', 'name': 'Common'}}; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) + item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; + _.each(common_nodes, function (innerNode) { + if (innerNode.length) + innerNode = (innerNode[0]['graph_id'] == compareGraphPage.graph_1_id) ? innerNode[0] : innerNode[1]; + if (item['data']['label'] == innerNode['label']) { + if (!common_id) { + common_id = item['data']['id']; + compareGraphPage.common_nodes['parent1'] = common_id; + } + item['data']['parent'] = 'common_1'; + } + }); + }); + }, + setGraphGroups: function (graph_json, common_nodes, graph_id) { + /** + * Params + * graph_json : nodes & edges datastructure + * common_nodes : nodes indentified by the comparison function + * graph_id : identity of graph + * + * This function assigns classes to elements of a graph based on its ID. + * This is required later to identify nodes/edges for each graphs. + * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs + * to be merged into a single graph. Classes help in identifying elements + * of individual graphs). + * Also, removes duplicate entries for common_nodes from the merged graph. + * + * It is called in compareGraphHelper() during comparison operation. + */ + var common_id = undefined; + var duplicate_nodes = []; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != (graph_id + 1)) + item['classes'] = 'graph_' + (graph_id + 1); + _.each(common_nodes, function (innerNode) { + if (innerNode.length) + innerNode = (innerNode[0]['graph_id'] == compareGraphPage.graph_ids[graph_id]) ? innerNode[0] : innerNode[1]; + if (item['data']['name'] == innerNode['name'] || item['data']['label'] == innerNode['label']) { + if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) + duplicate_nodes.push(item); + if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { + if (!common_id) { + common_id = item['data']['id']; + compareGraphPage.common_nodes['parent1'] = common_id; + } + item['classes'] = 'common_1'; + } + } + }); + }); + _.each(duplicate_nodes, function (node) { + graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); + }); + }, + setGraphClasses: function (graph_json, common_nodes, graph_id) { + /** + * Params + * graph_json : nodes & edges datastructure + * common_nodes : nodes indentified by the comparison function + * graph_id : identity of graph + * + * This function assigns a classes to elements of a graph based on its ID. + * This is required later to identify nodes/edges for each graphs. + * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs + * to be merged into a single graph. Classes help in identifying elements + * of individual graphs). + * Also, removes duplicate entries for common_nodes from the merged graph. + * + * It is called in compareGraphHelperMultiple() during comparison operation. + */ + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + var duplicate_nodes = []; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != (graph_id + 1)) + item['classes'] = 'graph_' + (graph_id + 1); + _.each(common_nodes, function (innerNode) { + if (!compareGraphPage.common_metadata[innerNode['name']]) + compareGraphPage.common_metadata[innerNode['name']] = []; + if (item['data']['name'] == innerNode['name'] || item['data']['label'] == innerNode['label']) { + let g_id = compareGraphPage.graph_ids[graph_id]; + if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) + duplicate_nodes.push(item); + if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { + if (!common_id) { + common_id = item['data']['id']; + } + item['classes'] = 'common_1'; + } + compareGraphPage.common_metadata[innerNode['name']].push(item['data']); + } + }); + }); + _.each(duplicate_nodes, function (node) { + graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); + }); + }, + setGraphGroupsMultiple: function (graph_json, common_nodes, graph_id) { + /** + * Params + * graph_json : nodes & edges datastructure + * common_nodes : nodes indentified by the comparison function + * graph_id : identity of graph + * + * This function assigns a group to elements of a graph based on its ID. + * This is required later to identify which graph nodes/edges belong to + * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs + * to be merged into a single graph grouping helps in identifying elements + * of individual graphs). + * Also, removes duplicate entries for common_nodes from the merged graph. + * + * It is called in compareGraphHelperMultiple() during comparison operation. + */ + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + var duplicate_nodes = []; + graph_json['elements']['nodes'][len] = { + 'data': { + 'id': 'graph_' + (graph_id + 1), + 'label': 'Graph ' + (graph_id + 1), + 'name': 'Graph ' + (graph_id + 1) + } + }; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) + item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; + _.each(common_nodes, function (innerNode) { + if (!compareGraphPage.common_metadata[innerNode['name']]) + compareGraphPage.common_metadata[innerNode['name']] = []; + if (item['data']['name'] == innerNode['name']) { + let g_id = compareGraphPage.graph_ids[graph_id]; + if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) + duplicate_nodes.push(item); + if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { + if (!common_id) { + common_id = item['data']['id']; + compareGraphPage.common_nodes['parent1'] = common_id; + } + item['data']['parent'] = 'common_1'; + } + compareGraphPage.common_metadata[innerNode['name']].push(item['data']); + } + }); + }); + // if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) { + _.each(duplicate_nodes, function (node) { + graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); + }); + // } + graph_json['elements']['nodes'][len - duplicate_nodes.length]['data']['parent'] = 'graph_2'; + }, + compareGraphsMultiple: function () { + /** + * This function is called when the user initiates graph comparison. + * Sends AJAX requests to load all the graphs selected by the user, + * it then calls compareGraphHelperMultiple(). + * + * Returns error whenever AJAX requests fail. + */ + $('#visualization-li a:first').tab('show'); + compareGraphPage.common_metadata = {}; + let graph_ids = [] + let count = 0; + for (let i = 0; i < 5; i++) { + if ($('#dropdownMenu' + (i + 3)).attr('value')) + graph_ids[i] = compareGraphPage.graph_ids[i] = $('#dropdownMenu' + (i + 3)).attr('value'); + if (graph_ids[i]) + count++; + } + operation = $('#operatorMenu1').attr('value') || $('#operatorMenu2').attr('value'); + for (let i = 0; i < count; i++) { + apis.graphs.getByID(graph_ids[i], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[i] = response['graph_json']; + compareGraphPage.styles_json[i] = response['style_json']; + $('#dropdownMenu' + (i + 3)).parent().find('a[row_id="' + graph_ids[i] + '"]').click(); + if (count == 1) { + console.log('All Graphs Loaded!'); + compareGraphPage.compareGraphHelperMultiple(); + } + count--; + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + } + }, + compareGraphs: function () { + /** + * This function is called when the user initiates graph comparison. + * Sends AJAX requests to load all the graphs selected by the user, + * it then calls compareGraphHelpe(). + * + * Returns error whenever AJAX requests fail. + */ + var temp_label = new Object(); + graph_1_id = compareGraphPage.graph_ids[0] = $('#dropdownMenu1').attr('value'); + graph_2_id = compareGraphPage.graph_ids[1] = $('#dropdownMenu2').attr('value'); + $('#visualization-li a:first').tab('show'); + operation = $('#operatorMenu1').attr('value'); + apis.graphs.getByID(compareGraphPage.graph_ids[0], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[0] = response['graph_json']; + compareGraphPage.styles_json[0] = response['style_json']; + apis.graphs.getByID(compareGraphPage.graph_ids[1], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[1] = response['graph_json']; + compareGraphPage.styles_json[1] = response['style_json']; + compareGraphPage.compareGraphHelper(); + compareGraphPage.setDropdownLabel($('#dropdownMenu1').parent().find('a[row_id="' + graph_1_id + '"]'), + $('#dropdownMenu1')); + compareGraphPage.setDropdownLabel($('#dropdownMenu2').parent().find('a[row_id="' + graph_2_id + '"]'), + $('#dropdownMenu2')); + // $('#dropdownMenu1').parent().find('a[row_id="' + graph_1_id + '"]').click(); + // $('#dropdownMenu2').parent().find('a[row_id="' + graph_2_id + '"]').click(); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + compareGraphVersions: function () { + /** + * This function is called when the user initiates graph comparison. + * Sends AJAX requests to load all the graphs selected by the user, + * it then calls compareGraphHelpe(). + * + * Returns error whenever AJAX requests fail. + */ + var temp_label = new Object(); + graph_1_id = compareGraphPage.graph_ids[0] = $('#dropdownMenu1').attr('value'); + graph_2_id = compareGraphPage.graph_ids[1] = $('#dropdownMenu2').attr('value'); + $('#visualization-li a:first').tab('show'); + operation = $('#operatorMenu1').attr('value'); + apis.version.getByID(compareGraphPage.graph_ids[0], version[0], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[0] = response['graph_json']; + compareGraphPage.styles_json[0] = response['style_json']; + apis.version.getByID(compareGraphPage.graph_ids[0], version[1], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[1] = response['graph_json']; + compareGraphPage.styles_json[1] = response['style_json']; + compareGraphPage.compareGraphHelper(); + compareGraphPage.setDropdownLabel($('#dropdownMenu1').parent().find('a[row_id="' + graph_1_id + '"]'), + $('#dropdownMenu1')); + compareGraphPage.setDropdownLabel($('#dropdownMenu2').parent().find('a[row_id="' + graph_2_id + '"]'), + $('#dropdownMenu2')); + // $('#dropdownMenu1').parent().find('a[row_id="' + graph_1_id + '"]').click(); + // $('#dropdownMenu2').parent().find('a[row_id="' + graph_2_id + '"]').click(); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + formatCyGraph: function () { + /** + * May be needed for future changes. + * Does graph formatting! + */ + compareGraphPage.cyGraph.filter(":parent").style({ + 'font-size': '0px', + }); + /*compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").layout({ + 'name': 'cola', + 'animate': false, + 'padding': 10 + }).run(); + compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").layout({ + 'name': 'cola', + 'animate': false, + 'padding': 10 + }).run(); + + compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").layout({ + 'name': 'cola', + 'animate': false, + 'padding': 10 + }).run();*/ + + let x_max = compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").max(function (ele, i, eles) { + return ele.position()['x']; + }); + let y_max = compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").max(function (ele, i, eles) { + return ele.position()['y']; + }); + y_max = Math.max(y_max.value, compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").max(function (ele, i, eles) { + return ele.position()['y']; + }).value); + + let cm_len_1 = compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").max(function (ele, i, eles) { + return ele.position()['x']; + }).value - compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").min(function (ele, i, eles) { + return ele.position()['x']; + }).value; + + compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").layout({ + 'name': 'cola', 'padding': 10, transform: (node) => { + let position = {}; + position.x = node.position('x') + x_max.value + 300; + position.y = node.position('y'); + return position; + } + }).run(); + + compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").layout({ + 'name': 'cola', 'padding': 10, transform: (node) => { + let position = {}; + position.x = node.position('x') + Math.abs(cm_len_1 / 2 - x_max.value) + 300; + position.y = node.position('y') + y_max + 550; + return position; + } + }).run(); + }, + setCommonElements: function (nodes) { + $.each(nodes, function (i, node) { + if (node.length > 1) { + let id_1 = compareGraphPage.cyGraph.nodes("[label = '" + node[0]['label'] + "']").id(); + let id_2 = compareGraphPage.cyGraph.nodes("[label = '" + node[1]['label'] + "']").id(); + + edges = compareGraphPage.cyGraph.nodes("[id = '" + id_1 + "']").connectedEdges(); + $.each(edges, function (i, edge) { + if (edge.data('source') == id_1) { + edge.move({source: id_2}); + } else + edge.move({target: id_2}); + }); + compareGraphPage.cyGraph.remove("node[id = '" + id_1 + "']"); + } + }); + + }, + compareGraphHelperMultiple: function () { + /** + * This function is called in compareGraphMultiple(). + * Sends AJAX request to execute Comparison functionality in backend. + * The AJAX request returns a list of nodes and edges identified by the comparison operation. + * Additionally, this function populates the Nodes and Edges table. + */ + let data = {}; + for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + data['graph_id_' + (i + 1)] = compareGraphPage.graph_ids[i]; + } + operation = $('#operatorMenu2').attr('value'); + data['operation'] = operation; + data['multiple'] = true; + apis.compare.get(data, + successCallback = function (response) { + console.log('Success'); + compareGraphPage.common_nodes = response['nodes']; + for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + compareGraphPage.setGraphClasses(compareGraphPage.graphs_json[i], response['nodes'], i); + // compareGraphPage.setGraphGroupsMultiple(compareGraphPage.graphs_json[i], response['nodes'], i); + if (i > 0) { + compareGraphPage.graphs_json[0]['elements']['nodes'] = + compareGraphPage.graphs_json[0]['elements']['nodes'] + .concat(compareGraphPage.graphs_json[i]['elements']['nodes']); + compareGraphPage.graphs_json[0]['elements']['edges'] = + compareGraphPage.graphs_json[0]['elements']['edges'] + .concat(compareGraphPage.graphs_json[i]['elements']['edges']); + } + + } + + compareGraphPage.cyGraph = compareGraphPage.constructCytoscapeGraph(compareGraphPage.graphs_json[0], compareGraphPage.styles_json[0]); + compareGraphPage.populateNodeDataMulti(response['nodes'], response['edges']); + for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + compareGraphPage.setNodesColor('graph_' + (i + 1), $('#colorpicker' + (i + 3)).val()); + } + + compareGraphPage.cyGraph.once('render', function () { // Wait for cytoscape to actually load and map eles + compareGraphPage.cyGraph.panzoom(); + window.setTimeout(function () { + compareGraphPage.cyGraph.reset().fit().center(); + // $('#cyGraphContainer').css('height', '99%'); + $('#visualization-li a:last').tab('show'); + $('#visualization-li a:first').tab('show'); + compareGraphPage.cyGraph.nodes().style("display", "element"); + $('#compareModal').modal('hide'); + }, 100); + }); + + // compareGraphPage.populateNodeData(response['nodes']); + // compareGraphPage.populateEdgeData(response['edges']); + + $('#nodes-total-badge').text(response['nodes'].length); + $('#edges-total-badge').text(response['edges'].length); + $('#operatorMenu1').attr('value', undefined); + $('#operatorMenu1').text('Select Operation'); + $('#operatorMenu1').append(''); + compareGraphPage.tabHelper(); + + }, + errorCallback = function (xhr, status, errorThrown) { + // This method is called when error occurs while deleting group_to_graph relationship. + $.notify({message: "You are not authorized to access one or more graphs selected for comparison."}, {type: 'danger'}); + }); + }, + tabHelper: function () { + /** + * Helper function to unhide information tabs in compare graph page. + * Additionally, initialize node and edge table as DataTable. + */ + $('#nodes-table').DataTable().draw(); + $('#edges-table').DataTable().draw(); + $('.dataTables_length').addClass('bs-select'); + + $('#nodes-li').show(); + $('#cyGraphContainer').show(); + $('#visualization-li').show(); + $('#edges-li').show(); + $('#visualization-li a:first').tab('show'); + }, + compareGraphHelper: function () { + /** + * This function is called in compareGraph(). + * Sends AJAX request to execute Comparison functionality in backend. + * The AJAX request returns a list of nodes and edges identified by the comparison operation. + * Additionally, this function populates the Nodes and Edges table. + */ + let data = ''; + operation = $('#operatorMenu1').attr('value'); + if (version){ + data = { + 'graph_id': compareGraphPage.graph_ids[0], + 'version': version, + 'operation': operation + } + } + else + data = { + 'graph_1_id': compareGraphPage.graph_ids[0], + 'graph_2_id': compareGraphPage.graph_ids[1], + 'operation': operation + } + + if (operation && compareGraphPage.graph_ids[0] && compareGraphPage.graph_ids[1]) { + $('#nodes-table > thead').find("th").remove(); + $('#edges-table > thead').find("th").remove(); + $('#nodes-table > thead > tr').append('

    Graph 1

    '); + $('#nodes-table > thead > tr').append('

    Graph 2

    '); + + $('#edges-table > thead > tr').append('

    Graph 1

    '); + $('#edges-table > thead > tr').append('

    Graph 2

    '); + + apis.compare.get(data, + successCallback = function (response) { + // $('#nodes-table').DataTable(); + compareGraphPage.common_nodes = response['nodes']; + + compareGraphPage.setGraphGroups(compareGraphPage.graphs_json[0], response['nodes'], 0); + compareGraphPage.setGraphGroups(compareGraphPage.graphs_json[1], response['nodes'], 1); + + compareGraphPage.graphs_json[0]['elements']['nodes'] = compareGraphPage.graphs_json[0]['elements']['nodes'].concat(compareGraphPage.graphs_json[1]['elements']['nodes']); + compareGraphPage.graphs_json[0]['elements']['edges'] = compareGraphPage.graphs_json[0]['elements']['edges'].concat(compareGraphPage.graphs_json[1]['elements']['edges']); + compareGraphPage.styles_json[0]['style'] = compareGraphPage.styles_json[0]['style'].concat(compareGraphPage.styles_json[1]['style']); + + compareGraphPage.cyGraph = compareGraphPage.constructCytoscapeGraph(compareGraphPage.graphs_json[0], compareGraphPage.styles_json[0]); + + compareGraphPage.populateNodeData(response['nodes']); + compareGraphPage.populateEdgeData(response['edges']); + + compareGraphPage.cyGraph.ready(function () { // Wait for cytoscape to actually load and map eles + + compareGraphPage.cyGraph.panzoom(); + window.setTimeout(function () { + // $('#cyGraphContainer').css('height', '99%'); + compareGraphPage.cyGraph.nodes().style("display", "element"); + }, 100); + }); + + compareGraphPage.cyGraph.once('render', function () { // Wait for cytoscape to actually load and map eles + window.setTimeout(function () { + compareGraphPage.cyGraph.reset().fit().center(); + // $('#cyGraphContainer').css('height', '99%'); + }, 100); + }); + + $('#nodes-total-badge').text(response['nodes'].length); + $('#edges-total-badge').text(response['edges'].length); + + compareGraphPage.setNodesColor('graph_1', $('#colorpicker1').val()); + compareGraphPage.setNodesColor('graph_2', $('#colorpicker2').val()); + compareGraphPage.setNodesColor('common_1', $('#operatorcolorpicker').val()); + compareGraphPage.tabHelper(); + }, + errorCallback = function (xhr, status, errorThrown) { + // This method is called when error occurs while deleting group_to_graph relationship. + $.notify({message: "You are not authorized to access one or more graphs selected for comparison."}, {type: 'danger'}); + }); + } else { + $.notify({message: "Please select correct parameters for Graph comparison."}, {type: 'danger'}); + } + }, + resetData: function () { + /** + * Hard reset compare page. + */ + location.replace("/compare"); + + }, + populateNodeDataMulti: function (nodes, edges) { + /** + * This function sets up the headers and body of the nodes table. + * This setup needs to be done dynamically as table columns are variable + * depending on the number of graphs selected for comparison operation. + * It is called in compareGraphHelperMulti() during graphs comparison. + */ + $('#result-view').empty(); + $('#result-view').append('
    \n' + + ' \n' + + ' \n' + + ' \n' + + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + '
    \n' + + '
    \n'); + $('#nodes-table > thead').find("th").remove(); + $('#nodes-comparison-table').empty(); + $('#edges-table > thead').find("th").remove(); + for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + $('#nodes-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + $('#edges-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + } + + var trHTML_nodes = ''; + var trHTML_edges = ''; + // $('#nodes-table').DataTable().clear().destroy(); + + + + $('#nodes-comparison-table').find("tr:gt(0)").remove(); + if (nodes.length && !nodes[0].length) { + // $('#nodes-table > thead').find("th:gt(0)").remove(); + $('#nodes-table').parent().attr('align', 'center'); + // $('#nodes-table').attr('style', 'width:800px;'); + + } else $('#nodes-table').attr('style', ''); + $.each(compareGraphPage.common_metadata, function (i, item) { + if (item.length) { + // Use 'name' field instead - for testing use 'label' + trHTML_nodes += '' + $.each(item, function (j, node) { + + trHTML_nodes += 'Name : ' + node['name'] + + '
    Label : ' + node['label']; + if (node['popup']) { + trHTML_nodes += '
    ' + node['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML_nodes += ''; + }); + trHTML_nodes += ''; + } + + }); + $('#nodes-comparison-table').append(trHTML_nodes); + + $('#edges-comparison-table').find("tr:gt(0)").remove(); + if (edges.length && !edges[0].length) { + $('#edges-table').parent().attr('align', 'center'); + + } else $('#edges-table').attr('style', ''); + $.each([], function (i, item) { + if (item.length) { + // Use 'name' field instead - for testing use 'label' + trHTML_edges += '' + $.each(item, function (j, node) { + + trHTML_edges += 'Name : ' + node['name'] + + '
    Label : ' + node['label']; + if (node['popup']) { + trHTML_edges += '
    ' + node['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML_edges += ''; + }); + trHTML_edges += ''; + } + + }); + $('#edges-comparison-table').append(trHTML_edges); + }, + populateNodeData: function (nodes) { + /** + * This function sets up the headers and body of the nodes table. + * This setup needs to be done dynamically as table columns are variable + * depending on the number of graphs selected for comparison operation. + * It is called in compareGraphHelperMulti() during graphs comparison. + */ + $('#result-view').empty(); + $('#result-view').append('
    \n' + + ' \n' + + ' \n' + + ' \n' + + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + '
    \n' + + '
    \n'); + $('#nodes-table > thead').find("th").remove(); + $('#nodes-comparison-table').empty(); + // $('#edges-table > thead').find("th").remove(); + let len = ($('#operatorMenu1').attr('value')=='intersection')?2:1; + + for (let i = 0; i < len; i++) { + $('#nodes-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + // $('#edges-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + } + var trHTML = ''; + var cyNode1 = undefined; + var cyNode2 = undefined; + // $('#nodes-table').DataTable().clear().destroy(); + $('#nodes-comparison-table').find("tr:gt(0)").remove(); + if (nodes.length && !nodes[0].length) { + // $('#nodes-table > thead').find("th:gt(0)").remove(); + $('#nodes-table').parent().attr('align', 'center'); + $('#nodes-table').attr('style', 'width:800px;'); + + } else $('#nodes-table').attr('style', ''); + $.each(nodes, function (i, item) { + if (item.length) { + // Use 'name' field instead - for testing use 'label' + cyNode1 = compareGraphPage.cyGraph.nodes("[label = '" + item[0]['label'] + "']"); + cyNode2 = compareGraphPage.cyGraph.nodes("[label = '" + item[1]['label'] + "']"); + + trHTML += 'Name : ' + item[0]['name'] + + '
    Label : ' + item[0]['label']; + if (cyNode1.length && cyNode1.data() && cyNode1.data()['popup']) { + trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ' Name : ' + item[1]['name'] + + '
    Label : ' + item[1]['label']; + + if (cyNode2.length && cyNode2.data() && cyNode2.data()['popup']) { + trHTML += '
    ' + cyNode2.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + + } else { + // Use 'name' field instead - for testing use 'label' + cyNode1 = compareGraphPage.cyGraph.getElementById(item['label']); + + trHTML += 'Name : ' + item['name'] + + '
    Label : ' + item['label']; + + if (cyNode1.length && cyNode1.data() && cyNode1.data()['popup']) { + trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + } + + }); + $('#nodes-comparison-table').append(trHTML); + }, + populateEdgeData: function (edges) { + /** + * This function sets up the headers and body of the edges table. + * This setup needs to be done dynamically as table columns are variable + * depending on the number of graphs selected for comparison operation. + * It is called in compareGraphHelperMulti() during graphs comparison. + */ + $('#result-view1').empty(); + $('#result-view1').append('
    \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '\n' + + ' \n' + + '
    \n' + + '
    \n'); + // $('#nodes-table > thead').find("th").remove(); + $('#edges-comparison-table').empty(); + $('#edges-table > thead').find("th").remove(); + let len = ($('#operatorMenu1').attr('value')=='intersection')?2:1; + + for (let i = 0; i < len; i++) { + // $('#nodes-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + $('#edges-table > thead > tr').append('

    Graph' + (i + 1) + '

    '); + } + var trHTML = ''; + var cyEdge1 = undefined; + var cyEdge2 = undefined; + $('#edges-table').DataTable().clear().destroy(); + $('#edges-comparison-table').find("tr:gt(0)").remove(); + if (edges.length && !edges[0].length) { + $('#edges-table > thead').find("th:gt(0)").remove(); + $('#edges-table').parent().attr('align', 'center'); + $('#edges-table').attr('style', 'width:800px;'); + } else $('#edges-table').attr('style', ''); + $.each(edges, function (i, item) { + + if (item.length) { + cyEdge1 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item[0]['name']]); + cyEdge2 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item[1]['name']]); + + trHTML += 'Name : ' + item[0]['name'] + + '
    Label : ' + item[0]['label']; + + if (cyEdge1.length && cyEdge1.data() && cyEdge1.data()['popup']) { + trHTML += '
    ' + cyEdge1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ' Name : ' + item[1]['name'] + + '
    Label : ' + item[1]['label']; + + if (cyEdge2.length && cyEdge2.data() && cyEdge2.data()['popup']) { + trHTML += '
    ' + cyEdge2.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ''; + + } else { + cyEdge1 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item['name']]); + + trHTML += 'Name : ' + item['name']; + + if (cyEdge1.length && cyEdge1.data() && cyEdge1.data()['popup']) { + trHTML += '
    ' + cyEdge1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + } + }); + $('#edges-comparison-table').append(trHTML); + }, + constructCytoscapeGraph: function (graph_json, style_json) { + /** + * Sets up a Cytoscape instance to represent the graph. + * It is called in compareGraphHelper() and compareGraphHelperMultiple(). + * Additionally, translates nodes/edges to for better visualization. + * It uses the Cola layout for rendering the graphs. + */ + layout = { + name: 'cola', + animate: true, + maxSimulationTime: 1000, + }; + let y_max = 0; + let y_min = Infinity; + let x_max = 0; + let c_max = 0; + let c_min = Infinity; + graph_json['elements']['nodes'] = _.map(graph_json['elements']['nodes'], function (node) { + var newNode = { + "data": node['data'] + }; + if ('position' in node) { + newNode['position'] = node['position']; + newNode['classes'] = node['classes'] + if (node['classes'] == 'graph_1') { + x_max = (x_max > node['position']['x']) ? x_max : node['position']['x']; + } + if (node['classes'] == 'common_1') { + c_max = (c_max > node['position']['x']) ? c_max : node['position']['x']; + c_min = (c_min < node['position']['x']) ? c_min : node['position']['x']; + } + y_max = (y_max > node['position']['y']) ? y_max : node['position']['y']; + y_min = (y_min < node['position']['y']) ? y_min : node['position']['y']; + /*layout = { + name: 'preset' + };*/ + } + + return newNode + }); + + _.map(graph_json['elements']['nodes'], function (node) { + if ('position' in node) { + + if (node['classes'] == 'graph_2') { + node['position']['x'] = node['position']['x'] + x_max + 100; + } + if (node['classes'] == 'common_1') { + node['position']['x'] = node['position']['x'] + Math.abs((c_max - c_min) / 2 - x_max) + 100; + node['position']['y'] = node['position']['y'] + y_max + 50; + } + } + }); + + + graph_json['elements']['edges'] = _.map(graph_json['elements']['edges'], function (edge) { + compareGraphPage.edge_name_to_id[edge['data']['name']] = edge['data']['id']; + return { + "data": edge['data'] + } + }); + + return cytoscape({ + container: document.getElementById('cyGraphContainer'), + boxSelectionEnabled: true, + autounselectify: false, + wheelSensitivity: 0.2, + minZoom: 1e-2, + maxZoom: 1e2, + elements: graph_json['elements'], + layout: layout, + + //Style properties of NODE body + style: _.concat(defaultStylesheet, cytoscapeGraph.parseStylesheet(style_json), selectedElementsStylesheet), + + ready: function () { + + //setup popup dialog for displaying dialog when nodes/edges + //are clicked for information. + $('#dialog').dialog({ + autoOpen: false + }); + this.nodes().style("display", "none"); + // display node data as a popup + this.on('tap', graphPage.onTapGraphElement); + + } + }); + + }, +}; \ No newline at end of file diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index a7f4c57a..7898761d 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -39,6 +39,15 @@ var apis = { apis.jsonRequest('GET', apis.nodes.ENDPOINT({'graph_id': graph_id}), data, successCallback, errorCallback) }, }, + version: { + ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/version/'), + get: function (graph_id, data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.version.ENDPOINT({'graph_id': graph_id}), data, successCallback, errorCallback) + }, + getByID: function (graph_id, version_id, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.version.ENDPOINT({'graph_id': graph_id}) + version_id, undefined, successCallback, errorCallback) + }, + }, edges: { ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/edges/'), get: function (graph_id, data, successCallback, errorCallback) { @@ -63,6 +72,36 @@ var apis = { apis.jsonRequest('DELETE', apis.layouts.ENDPOINT({'graph_id': graph_id}) + layout_id, undefined, successCallback, errorCallback) } }, + compare: { + ENDPOINT: '/ajax/compare/', + get: function (data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.compare.ENDPOINT, data, successCallback, errorCallback) + } + }, + compareMultiple: { + ENDPOINT: '/ajax/compare/multi', + get: function (data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.compare.ENDPOINT, data, successCallback, errorCallback) + } + }, + compatibility: { + ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/version/<%= graph_version_id %>/compatibility/'), + get: function (graph_id, graph_version_id, layout_id, data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.compatibility.ENDPOINT({'graph_id': graph_id, 'graph_version_id': graph_version_id}) + layout_id, data, successCallback, errorCallback) + }, + add: function (graph_id, graph_version_id, layout_id, data, successCallback, errorCallback) { + apis.jsonRequest('POST', apis.compatibility.ENDPOINT({'graph_id': graph_id, 'graph_version_id': graph_version_id}) + layout_id, data, successCallback, errorCallback) + }, + getByID: function (graph_id, graph_version_id, layout_id, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.compatibility.ENDPOINT({'graph_id': graph_id, 'graph_version_id': graph_version_id}) + layout_id, undefined, successCallback, errorCallback) + }, + update: function (graph_id, graph_version_id, layout_id, data, successCallback, errorCallback) { + apis.jsonRequest('PUT', apis.compatibility.ENDPOINT({'graph_id': graph_id, 'graph_version_id': graph_version_id}) + layout_id, data, successCallback, errorCallback) + }, + delete: function (graph_id, graph_version_id, layout_id, successCallback, errorCallback) { + apis.jsonRequest('DELETE', apis.compatibility.ENDPOINT({'graph_id': graph_id, 'graph_version_id': graph_version_id}) + layout_id, undefined, successCallback, errorCallback) + } + }, logging: { ENDPOINT: _.template('http://<%= hostname %>:9200/layouts/action'), add: function (data, successCallback, errorCallback) { @@ -216,6 +255,12 @@ var graphsPage = { var queryString = (graphsPage.searchBar.val() && graphsPage.searchBar.val().length > 0) ? '?query=' + _.join(graphsPage.searchBar.val(), ',') : ''; return $('').attr('href', '/graphs/' + row.id + queryString).text(value)[0].outerHTML; }, + + graphCheckBoxFormatter: function (value, row) { + var checkbox = $('').attr('type', 'checkbox').attr('row_id', row.id).attr('onclick', 'compareGraphPage.selectGraphForCompare($(this),' + row.id +')').text(value)[0].outerHTML; + return checkbox + $('').attr('href', '/graphs/' + row.id).text(value)[0].outerHTML; + }, + tagsFormatter: function (value, row) { links = []; links = _.map(row.tags, function (tag) { @@ -427,9 +472,15 @@ var uploadGraphPage = { } }; + + var graphPage = { cyGraph: undefined, timeout: null, + currentVersionID: null, + currentLayoutID: null, + elementsWithoutStyle: [], + unidentifiedLayoutElements: [], init: function () { /** * This function is called to setup the graph page. @@ -452,9 +503,6 @@ var graphPage = { }); $('#saveLayoutBtn').click(function () { - - cytoscapeGraph.showGraphInformation(graphPage.cyGraph); - graphPage.saveLayout($('#saveLayoutNameInput').val(), '#saveLayoutModal'); }); @@ -480,6 +528,37 @@ var graphPage = { graphPage.cyGraph.unbind('tap').on('tap', graphPage.onTapGraphElement); }); + $('#layoutCompatibilityModal').on('hidden.bs.modal', function () { + current_layout = $("#current_layout_label").text(); + if( graphPage.elementsWithoutStyle.length ){ + $('#tickerBar').empty(); + $('#tickerBar').append($('', { + style : "padding:5px; display: block; background:#DB3847; color: white", + }).html('ERROR :  ' + graphPage.elementsWithoutStyle.length + ' elements of ' + current_layout + ' are not compatible with the current version of the Graph. ').append($('', { + style : 'color:inherit', + 'id': 'tickerBarInfo', + 'onclick': "$('#layoutCompatibilityModal').modal('show');" + }).html('Click here to view more information!')) + ); + } + else if( graphPage.unidentifiedLayoutElements.length ){ + $('#tickerBar').empty(); + $('#tickerBar').append($('', { + style : "padding:5px; display: block; background:#FFC010; color: black", + }).html('WARNING :  ' + current_layout + ' contains additional ' + graphPage.unidentifiedLayoutElements.length + ' elements absent in the current version of the Graph. ').append($('', { + style : 'color:inherit', + 'id': 'tickerBarInfo', + 'onclick': "$('#layoutCompatibilityModal').modal('show');" + }).html('Click here to view more information!')).append($('' + ].join(''); + } + else + return [ + '' + ].join(''); + }, + setDefaultVersion: function (version_id) { + console.log(version_id); + apis.graphs.update($('#GraphID').val(), { + 'default_version_id': version_id + }, + successCallback = function (response) { + default_version_id = response.default_version_id; + $("button[name='setGraphVersion']").removeClass('disabled'); + $("button[name='setGraphVersion']").text('Set as default'); + $("button[version_id=" + version_id + "]").addClass('disabled'); + $("button[version_id=" + version_id + "]").text('Default Version'); + //$("#GraphVersionTable").find('tr').removeClass('success'); + //$("#GraphVersionTable").find('span[row_id=' + version_id + ']').parent().parent().addClass('success'); + + }, + errorCallback = function (xhr, status, errorThrown) { + // This method is called when error occurs while deleting group_to_graph relationship. + $.notify({ + message: response.responseJSON.error_message + }, { + type: 'danger' + }); + }); + } + }, + layoutCompatibilityTable: { + getConflictingLayoutElements: function (param) { + param.success({'data': graphPage.elementsWithoutStyle}); + return []; + }, + getUnidentifiedLayoutElements: function (param) { + param.success({'data': graphPage.unidentifiedLayoutElements}); + return []; + }, + elementsFormatter: function (value, row, index) { + //$("#version_selector_dropdown").append('
  • ' + value + '
  • ') + element_type = value.split('[')[0]; + element_type = element_type[0].toUpperCase() + element_type.slice(1); + value = value.split('\'')[1]; + + return [ + '
    ', + element_type=='Node'?row.style.label:value, + '
    ', element_type, '', + '
    ', + '
    ', + '  Add Style ' + '  ', + '
    ' + ].join(''); + }, + unknownElementsFormatter: function (value, row, index) { + element_type = value.split('[')[0]; + element_type = element_type[0].toUpperCase() + element_type.slice(1); + value = value.split('\'')[1]; + return [ + '
    ', + element_type=='Node'?row.style.label:value, + '
    ', element_type, '', + '
    ', + ].join(''); + }, + operationEvents: { + 'click .edit-layout': function (e, value, row, index) { + $('#nodeEditorSideBar1').addClass('active'); + graphPage.onSelectLayoutCompBtnClick(row.name, index); + $('#editLayoutCompatibilityModal').data('index', index).modal('show'); + } + }, + selectorFormatter: function (value, row, index) { + return (''+ value +'') + }, + styleFormatter: function (value, row, index) { + return (''+ JSON.stringify(value) +'') + }, + }, layoutsTable: { getPrivateLayoutsByGraphID: function (params) { /** @@ -1150,11 +1618,24 @@ var graphPage = { if (response.total > 0) { $('#selectSavedLayoutHeading').show(); + $("#layout_selector_dropdown").append($('
  • ', {class:"dropdown-header"}).html('Select Private Layout')); + _.each(response.layouts, function (layout) { + graphPage.addLayoutBtns(layout, 'userPrivateLayoutBtns'); + dropdown_value = layout.name + if (dropdown_value.length >15){ + dropdown_value = dropdown_value.slice(0,15) + '...'; + } + $("#layout_selector_dropdown").append($('
  • ').append($('', { + 'row_id' : layout.id, + 'data-layout-id' : layout.id, + 'onclick' :"graphPage.onSelectLayoutBtnClick(this)" + }).html(dropdown_value))); + + }); } $('#userPrivateLayoutBtns').html(''); - _.each(response.layouts, function (layout) { - graphPage.addLayoutBtns(layout, 'userPrivateLayoutBtns'); - }); + + }, errorCallback = function () { // This method is called when error occurs while fetching layouts. @@ -1183,11 +1664,23 @@ var graphPage = { if (response.total > 0) { $('#selectSavedLayoutHeading').show(); + $("#layout_selector_dropdown").append($('
  • ', {class:"dropdown-header"}).html('Select Shared Layout')); + _.each(response.layouts, function (layout) { + graphPage.addLayoutBtns(layout, 'userSharedLayoutBtns'); + dropdown_value = layout.name + if (dropdown_value.length >15){ + dropdown_value = dropdown_value.slice(0,15) + '...'; + } + $("#layout_selector_dropdown").append($('
  • ').append($('', { + 'row_id' : layout.id, + 'data-layout-id' : layout.id, + 'onclick' :"graphPage.onSelectLayoutBtnClick(this)" + }).html(dropdown_value))); + }); } $('#userSharedLayoutBtns').html(''); - _.each(response.layouts, function (layout) { - graphPage.addLayoutBtns(layout, 'userSharedLayoutBtns'); - }); + + }, errorCallback = function () { // This method is called when error occurs while fetching layouts. @@ -1386,7 +1879,7 @@ var graphPage = { graphPage.cyGraph.on('free', function (e) { - var selected_elements = e.cyTarget.length > 1 ? graphPage.cyGraph.elements(':selected') : e.cyTarget; + var selected_elements = e.target.length > 1 ? graphPage.cyGraph.elements(':selected') : e.target; graphPage.layoutEditor.undoRedoManager.update({ 'action_type': 'move_node', 'data': { @@ -1489,7 +1982,7 @@ var graphPage = { } }); - graphPage.cyGraph.elements().on('select, unselect', function () { + graphPage.cyGraph.elements().on('select unselect', function () { if (graphPage.cyGraph.nodes(':selected').length > 0) { $('#editSelectedNodesBtn').removeClass('disabled'); } else { @@ -1631,6 +2124,12 @@ var graphPage = { $('#nodeLabel').val(collection.style('content')); $("#nodeBackgroundColorPicker").colorpicker('setValue', collection.style('background-color')); + + $('#nodeShape1').val(collection.style('shape')); + $('#nodeWidth1').val(_.replace(collection.style('width'), 'px', '')); + $('#nodeHeight1').val(_.replace(collection.style('height'), 'px', '')); + $('#nodeLabel1').val(collection.style('content')); + $("#nodeBackgroundColorPicker1").colorpicker('setValue', collection.style('background-color')); } else { $('#nodeShape').val(null); $('#nodeWidth').val(null); @@ -1639,7 +2138,29 @@ var graphPage = { $("#nodeBackgroundColorPicker").unbind('changeColor').colorpicker('setValue', null); $('#nodeBackgroundColorPicker').on('changeColor', graphPage.layoutEditor.nodeEditor.onNodeBackgroudColorChange); + + $('#nodeShape1').val(null); + $('#nodeWidth1').val(null); + $('#nodeHeight1').val(null); + $('#nodeLabel1').val(null); + $("#nodeBackgroundColorPicker1").unbind('changeColor').colorpicker('setValue', null); + $('#nodeBackgroundColorPicker1').on('changeColor', graphPage.layoutEditor.nodeEditor.changeNodeColor); } + $(".colorpicker.dropdown-menu").css('z-index', '20500'); + }, + changeNodeColor: function () { + if (_.isEmpty($("#nodeBackgroundColorPicker1").colorpicker('getValue'))) { + return $.notify({ + message: 'Please enter valid color value!', + }, { + type: 'warning' + }); + } else { + + $('#nodeBackgroundColorPicker').colorpicker('setValue', $("#nodeBackgroundColorPicker1").colorpicker('getValue')) + } + + }, updateNodeProperty: function (styleJSON) { nodeSelector = _.template("node[name='<%= name %>']"); @@ -1664,6 +2185,7 @@ var graphPage = { }); $('.gs-sidebar-nav').removeClass('active'); $('#nodeEditorSideBar').addClass('active'); + $('#nodeEditorSideBar1').addClass('active'); graphPage.layoutEditor.nodeEditor.init(); $('#nodeWidth').on('input', function (e) { @@ -1718,6 +2240,57 @@ var graphPage = { }); } }); + + + $('#nodeWidth1').on('input', function (e) { + if (!_.isEmpty($('#nodeWidth1').val())) { + $('#nodeWidth').val($('#nodeWidth1').val()); + $('#nodeWidth').trigger('input'); + } + else { + return $.notify({ + message: 'Please enter valid width value!', + }, { + type: 'warning' + }); + } + }); + + $('#nodeHeight1').on('input', function (e) { + if (!_.isEmpty($('#nodeHeight1').val())) { + $('#nodeHeight').val($('#nodeHeight1').val()); + $('#nodeHeight').trigger('input'); + } + else { + return $.notify({ + message: 'Please enter valid height value!', + }, { + type: 'warning' + }); + } + }); + + $('#nodeShape1').on('change', function (e) { + if (_.isEmpty($('#nodeShape1').val())) { + return $.notify({ + message: 'Please enter valid shape value!', + }, { + type: 'warning' + }); + } else { + $('#nodeShape').val($('#nodeShape1').val()); + $('#nodeShape').trigger('change'); + } + }); + + $('#nodeLabel1').on('input', function (e) { + if (!_.isEmpty($('#nodeLabel1').val())) { + $('#nodeLabel').val($('#nodeLabel1').val()); + $('#nodeLabel').trigger('input'); + } + }); + + $('#nodeBackgroundColorPicker1').on('changeColor', graphPage.layoutEditor.nodeEditor.changeNodeColor); }, close: function (save) { if (save) { @@ -1731,11 +2304,21 @@ var graphPage = { } }); graphPage.layoutEditor.nodeEditor.styleBeforeEdit = null; + index = $('#editLayoutCompatibilityModal').data('index'); + if (graphPage.elementsWithoutStyle.length){ + graphPage.elementsWithoutStyle.splice(index, 1); + } + else { + $('#tickerBar').empty(); + } + $('#LayoutCompatibilityTable').bootstrapTable('refresh'); } else { cytoscapeGraph.applyStylesheet(graphPage.cyGraph, graphPage.layoutEditor.nodeEditor.styleBeforeEdit); } $('.gs-sidebar-nav').removeClass('active'); $('#layoutEditorSideBar').addClass('active'); + $('#editLayoutCompatibilityModal').modal('hide'); + if (graphPage.elementsWithoutStyle.length==0) $('#tickerBar').empty(); }, onNodeBackgroudColorChange: function (e) { if (_.isEmpty($("#nodeBackgroundColorPicker").colorpicker('getValue'))) { @@ -1763,6 +2346,13 @@ var graphPage = { $('#edgeSourceArrowShape').val(collection.style('source-arrow-shape')); $('#edgeTargetArrowShape').val(collection.style('target-arrow-shape')); $("#edgeLineColorPicker").colorpicker('setValue', collection.style('line-color')); + + + $('#edgeWidth1').val(_.replace(collection.style('width'), 'px', '')); + $('#edgeStyle1').val(collection.style('line-style')); + $('#edgeSourceArrowShape1').val(collection.style('source-arrow-shape')); + $('#edgeTargetArrowShape1').val(collection.style('target-arrow-shape')); + $("#edgeLineColorPicker1").colorpicker('setValue', collection.style('line-color')); collection.select(); } else { $('#edgeWidth').val(null); @@ -1771,7 +2361,31 @@ var graphPage = { $("#edgeLineColorPicker").unbind('changeColor').colorpicker('setValue', null); $('#edgeLineColorPicker').on('changeColor', graphPage.layoutEditor.edgeEditor.onEdgeLineColorChange); + + + $('#edgeWidth1').val(null); + $('#edgeStyle1').val(null); + + + $("#edgeLineColorPicker1").unbind('changeColor').colorpicker('setValue', null); + $('#edgeLineColorPicker1').on('changeColor', graphPage.layoutEditor.edgeEditor.changeEdgeColor); + } + $(".colorpicker.dropdown-menu").css('z-index', '20500'); + }, + changeEdgeColor: function () { + if (_.isEmpty($("#edgeLineColorPicker1").colorpicker('getValue'))) { + return $.notify({ + message: 'Please enter valid color value!', + }, { + type: 'warning' + }); + } else { + $('#edgeLineColorPicker').colorpicker('setValue', $("#edgeLineColorPicker1").colorpicker('getValue')) + //$('#nodeBackgroundColorPicker').val($("#nodeBackgroundColorPicker1").colorpicker('getValue')); + //$('#nodeBackgroundColorPicker').trigger('change'); } + + }, onEdgeLineColorChange: function (e) { @@ -1813,12 +2427,22 @@ var graphPage = { 'metadata': layoutLearner.computeLayoutMetadata(graphPage.cyGraph) } }); + index = $('#editLayoutCompatibilityModal').data('index'); + if (graphPage.elementsWithoutStyle.length){ + graphPage.elementsWithoutStyle.splice(index, 1); + } + else { + $('#tickerBar').empty(); + } + $('#LayoutCompatibilityTable').bootstrapTable('refresh'); graphPage.layoutEditor.edgeEditor.styleBeforeEdit = null; } else { cytoscapeGraph.applyStylesheet(graphPage.cyGraph, graphPage.layoutEditor.edgeEditor.styleBeforeEdit); } $('.gs-sidebar-nav').removeClass('active'); $('#layoutEditorSideBar').addClass('active'); + $('#editLayoutCompatibilityModal').modal('hide'); + if (graphPage.elementsWithoutStyle.length==0) $('#tickerBar').empty(); }, open: function (collection) { collection.unselect(); @@ -1831,6 +2455,7 @@ var graphPage = { $('.gs-sidebar-nav').removeClass('active'); $('#edgeEditorSideBar').addClass('active'); + $('#edgeEditorSideBar1').addClass('active'); graphPage.layoutEditor.edgeEditor.init(); $('#edgeWidth').on('input', function (e) { @@ -1889,8 +2514,62 @@ var graphPage = { } }); - $('#nodeBackgroundColorPicker').on('changeColor', graphPage.layoutEditor.edgeEditor.onEdgeLineColorChange); + //$('#nodeBackgroundColorPicker').on('changeColor', graphPage.layoutEditor.edgeEditor.onEdgeLineColorChange); + $('#edgeLineColorPicker').on('changeColor', graphPage.layoutEditor.edgeEditor.onEdgeLineColorChange); + + $('#edgeLineColorPicker1').on('changeColor', graphPage.layoutEditor.edgeEditor.changeEdgeColor); + + $('#edgeWidth1').on('input', function (e) { + if (_.isEmpty($('#edgeWidth1').val())) { + return $.notify({ + message: 'Please enter valid width value!', + }, { + type: 'warning' + }); + } else { + $('#edgeWidth').val($('#edgeWidth1').val()); + $('#edgeWidth').trigger('input'); + } + }); + + $('#edgeStyle1').on('change', function (e) { + if (_.isEmpty($('#edgeStyle1').val())) { + return $.notify({ + message: 'Please enter valid style value!', + }, { + type: 'warning' + }); + } else { + $('#edgeStyle').val($('#edgeStyle1').val()); + $('#edgeStyle').trigger('change'); + } + }); + + $('#edgeSourceArrowShape1').on('change', function (e) { + if (_.isEmpty($('#edgeSourceArrowShape1').val())) { + return $.notify({ + message: 'Please enter valid arrow shape value!', + }, { + type: 'warning' + }); + } else { + $('#edgeSourceArrowShape').val($('#edgeSourceArrowShape1').val()); + $('#edgeSourceArrowShape').trigger('change'); + } + }); + $('#edgeTargetArrowShape1').on('change', function (e) { + if (_.isEmpty($('#edgeTargetArrowShape1').val())) { + return $.notify({ + message: 'Please enter valid arrow shape value!', + }, { + type: 'warning' + }); + } else { + $('#edgeTargetArrowShape').val($('#edgeTargetArrowShape1').val()); + $('#edgeTargetArrowShape').trigger('change'); + } + }); } } @@ -1969,43 +2648,6 @@ var graphPage = { } return largestK; }, - applyMax: function (graph_layout) { - //Gets all nodes and edges up do the max value set - //and only renders them - var maxVal = parseInt($("#input_max").val()); - - if (!maxVal) { - return; - } - var newJSON = { - "nodes": new Array(), - "edges": new Array() - }; - - // List of node ids that should remain in the graph - var nodeNames = Array(); - - //Get all edges that meet the max quantifier - for (var i = 0; i < graph_json.elements['edges'].length; i++) { - var edge_data = graph_json.elements['edges'][i]; - if (edge_data['data']['k'] <= maxVal) { - newJSON['edges'].push(edge_data); - nodeNames.push(edge_data['data']['source']); - nodeNames.push(edge_data['data']['target']); - } - } - - //Get all nodes that meet the max quantifier - for (var i = 0; i < graph_json.elements['nodes'].length; i++) { - var node_data = graph_json.elements['nodes'][i]; - if (nodeNames.indexOf(node_data['data']['id']) > -1) { - newJSON['nodes'].push(node_data); - } - } - - graphPage.cyGraph.load(newJSON); - graphPage.filterNodesEdges.showOnlyK(); - }, showOnlyK: function () { // Returns all the id's that are > k value if ($("#input_k").val()) { @@ -2338,43 +2980,6 @@ var graphPage = { max: 50 }); }, - applyMax: function (graph_layout) { - //Gets all nodes and edges up do the max value set - //and only renders them - var maxVal = parseInt($("#input_max").val()); - - if (!maxVal) { - return; - } - var newJSON = { - "nodes": new Array(), - "edges": new Array() - }; - - // List of node ids that should remain in the graph - var nodeNames = Array(); - - //Get all edges that meet the max quantifier - for (var i = 0; i < graph_json.elements['edges'].length; i++) { - var edge_data = graph_json.elements['edges'][i]; - if (edge_data['data']['k'] <= maxVal) { - newJSON['edges'].push(edge_data); - nodeNames.push(edge_data['data']['source']); - nodeNames.push(edge_data['data']['target']); - } - } - - //Get all nodes that meet the max quantifier - for (var i = 0; i < graph_json.elements['nodes'].length; i++) { - var node_data = graph_json.elements['nodes'][i]; - if (nodeNames.indexOf(node_data['data']['id']) > -1) { - newJSON['nodes'].push(node_data); - } - } - - graphPage.cyGraph.load(newJSON); - graphPage.filterNodesEdges.showOnlyK(); - }, setBarToValueEdgeLength: function (inputId, barId) { /** * If the user enters a value greater than the max value allowed, change value of bar to max allowed value. @@ -2443,7 +3048,7 @@ var cytoscapeGraph = { title: 'edit selected nodes', selector: 'node', onClickFunction: function (event) { - graphPage.layoutEditor.nodeEditor.open(cy.collection(cy.elements(':selected')).add(event.cyTarget).select()); + graphPage.layoutEditor.nodeEditor.open(cy.collection(cy.elements(':selected')).add(event.target).select()); }, hasTrailingDivider: true }, @@ -2453,7 +3058,7 @@ var cytoscapeGraph = { selector: 'node', show: true, onClickFunction: function (event) { - selectAllOfTheSameType(event.cyTarget); + selectAllOfTheSameType(event.target); } }, { @@ -2462,7 +3067,7 @@ var cytoscapeGraph = { selector: 'node', show: true, onClickFunction: function (event) { - unselectAllOfTheSameType(event.cyTarget); + unselectAllOfTheSameType(event.target); } }, { @@ -2471,7 +3076,7 @@ var cytoscapeGraph = { selector: 'edge', show: true, onClickFunction: function (event) { - selectAllOfTheSameType(event.cyTarget); + selectAllOfTheSameType(event.target); } }, { @@ -2480,7 +3085,7 @@ var cytoscapeGraph = { selector: 'edge', show: true, onClickFunction: function (event) { - unselectAllOfTheSameType(event.cyTarget); + unselectAllOfTheSameType(event.target); } } ] @@ -2784,7 +3389,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: false, padding: 0 - }); + }).run(); } else if (layout_name === "fill_circle") { collection.layout( { @@ -2792,7 +3397,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: false, padding: 40 - }); + }).run(); } else if (layout_name === "grid") { collection.layout( { @@ -2800,7 +3405,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: true, condense: true - }); + }).run(); } else if (layout_name === "square") { cytoscapeGraph.runSquareLayoutOnCollection(cy, collection); } else if (layout_name === "horizontal") { diff --git a/static/js/groups_page.js b/static/js/groups_page.js index be5f3c6e..b7e88453 100644 --- a/static/js/groups_page.js +++ b/static/js/groups_page.js @@ -72,6 +72,8 @@ var groupsPage = { submit: function (e) { e.preventDefault(); + $('#CreateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val(), @@ -79,7 +81,13 @@ var groupsPage = { }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#CreateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return; } apis.groups.add(group, @@ -89,7 +97,21 @@ var groupsPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while adding group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#CreateGroupBtn').attr('disabled', false); }); } }, @@ -282,13 +304,21 @@ var groupPage = { submit: function (e) { e.preventDefault(); + $('#UpdateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val() }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#UpdateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return } apis.groups.update($('#GroupID').val(), group, @@ -298,7 +328,21 @@ var groupPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while updating group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#UpdateGroupBtn').attr('disabled', false); }); } }, diff --git a/static/js/main.js b/static/js/main.js index aefeaf82..47fe3cd0 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -62,6 +62,18 @@ var header = { var password = $("#password").val(); var verify_password = $("#verify_password").val(); + if ($("#user_id")) { + var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + if (reg.test(user_id) == false) { + $.notify({ + message: 'Please enter a valid email address!' + }, { + type: 'warning' + }); + return; + } + } + if (!$("#user_id") || user_id.length == 0) { $.notify({ message: 'Please enter your email!' diff --git a/templates/base.html b/templates/base.html index 601bbf03..1f7525d7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,7 +17,7 @@ - + @@ -47,7 +47,7 @@ - + diff --git a/templates/compare_graph/compare_graphs.html b/templates/compare_graph/compare_graphs.html new file mode 100644 index 00000000..9e09ea22 --- /dev/null +++ b/templates/compare_graph/compare_graphs.html @@ -0,0 +1,188 @@ +{% extends 'base.html' %} +{% block content %} + {% load staticfiles %} + + +{% include 'compare_graph/compare_modal.html' %} + +
    + + + + +
    + +
    + {% include 'compare_graph/nodes_table.html' %} +
    + +
    + {% include 'compare_graph/edges_table.html' %} +
    + +
    + {% include 'compare_graph/compare_visualization.html' %} +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/compare_graph/compare_modal.html b/templates/compare_graph/compare_modal.html new file mode 100644 index 00000000..47678fd3 --- /dev/null +++ b/templates/compare_graph/compare_modal.html @@ -0,0 +1,143 @@ + \ No newline at end of file diff --git a/templates/compare_graph/compare_visualization.html b/templates/compare_graph/compare_visualization.html new file mode 100644 index 00000000..75068925 --- /dev/null +++ b/templates/compare_graph/compare_visualization.html @@ -0,0 +1,7 @@ +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/templates/compare_graph/edges_table.html b/templates/compare_graph/edges_table.html new file mode 100644 index 00000000..645ff26c --- /dev/null +++ b/templates/compare_graph/edges_table.html @@ -0,0 +1,14 @@ +
    + + + + {# #} + {# #} + + + + + +

    Primary Graph

    Secondary Graph

    +
    +
    \ No newline at end of file diff --git a/templates/compare_graph/nodes_table.html b/templates/compare_graph/nodes_table.html new file mode 100644 index 00000000..03977fc4 --- /dev/null +++ b/templates/compare_graph/nodes_table.html @@ -0,0 +1,15 @@ +
    + + + + {# #} + {# #} + + + + + +

    Primary Graph

    Secondary Graph

    + +
    +
    diff --git a/templates/graph/default_sidebar.html b/templates/graph/default_sidebar.html index 9214c445..30145fb5 100644 --- a/templates/graph/default_sidebar.html +++ b/templates/graph/default_sidebar.html @@ -34,7 +34,13 @@ Change Layout
  • + {% if uid %} +
  • + + Save Layout + +
  • Use Layout
    Editor diff --git a/templates/graph/graph_details_tab.html b/templates/graph/graph_details_tab.html index de38caa3..ee24f498 100644 --- a/templates/graph/graph_details_tab.html +++ b/templates/graph/graph_details_tab.html @@ -28,6 +28,23 @@ {% endfor %} + + + + + + {% if shared_groups %} + {% for k in shared_groups %} + + + + {% endfor %} + {% else %} + + + + {% endif %} +
    Shared with groups
    {{k.name|safe}}
    'The graph is not shared with any groups'
    diff --git a/templates/graph/graph_version_table.html b/templates/graph/graph_version_table.html new file mode 100644 index 00000000..a3489523 --- /dev/null +++ b/templates/graph/graph_version_table.html @@ -0,0 +1,29 @@ + + + + + + {# #} + + + + + + +
    Version NameDescriptionLast#} + {# Modified#} + {# Created at + Operations +
    diff --git a/templates/graph/index.html b/templates/graph/index.html index 830d44f3..7f865bf1 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -6,12 +6,14 @@ var graph_json = {{ graph_json_string|safe }}; var style_json = {{ style_json_string|safe }}; var default_layout_id = {% if default_layout_id %}{{ default_layout_id|safe }}{% else %}null{% endif %}; + var default_version_id = {% if default_version_id %}{{ default_version_id|safe }}{% else %}null{% endif %}; {% endif %}
  • {% endif %} - + {% if uid %} +
  • + Graph Version +
  • + {% endif %} + {% if uid %} + + {% endif %} + +
    + + +
    +
    + + +
    + +
    + +
    @@ -274,7 +314,12 @@

    {% include 'graph/layouts_table.html' %}
    - +
    + {% include 'graph/graph_version_table.html' %} +
    +

    @@ -298,7 +343,7 @@

    - diff --git a/templates/graph/layout_compatibility_modal.html b/templates/graph/layout_compatibility_modal.html new file mode 100644 index 00000000..f2fa171b --- /dev/null +++ b/templates/graph/layout_compatibility_modal.html @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/templates/graph/layout_compatibility_table.html b/templates/graph/layout_compatibility_table.html new file mode 100644 index 00000000..22b39f35 --- /dev/null +++ b/templates/graph/layout_compatibility_table.html @@ -0,0 +1,238 @@ + + + + \ No newline at end of file diff --git a/templates/graphs/error.html b/templates/graphs/error.html index 95eac32a..00b83808 100644 --- a/templates/graphs/error.html +++ b/templates/graphs/error.html @@ -18,6 +18,6 @@

    {{ Error | safe}}

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/graphs/index.html b/templates/graphs/index.html index 302708f2..0700cff7 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -26,6 +26,9 @@
  • Upload New Graph
  • +
  • + Compare Graphs +
  • @@ -50,9 +53,21 @@ wnt,tags:kegg-networks paper_title:Xtalk
    + + + + @@ -73,6 +88,11 @@
    Upload Graph
    + +
    + Compare Graphs +
    + {% include 'graphs/delete_graph_modal.html' %} @@ -83,6 +103,7 @@ +