diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..9de17e17 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Purpose +_Describe the problem or feature in addition to a link to the issues._ + +Example: +Fixes # . + +## Approach +_How does this change address the problem?_ + +#### Open Questions and Pre-Merge TODOs +- [ ] Use github checklists. When solved, check the box and explain the answer. + +## Learning +_Describe the research stage_ + +_Links to blog posts, patterns, libraries or addons used to solve this problem_ + +#### Blog Posts +- [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template. + 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..b2526644 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,90 @@ 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 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..41bd8e56 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -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,157 @@ 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 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..2f051a27 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -17,8 +17,10 @@ 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) + #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 +34,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 +63,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 +72,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 +288,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..2817f506 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -27,6 +27,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 +50,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..877e375e 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -113,7 +113,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 +504,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 +1525,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/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/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/static/css/graphspace.css b/static/css/graphspace.css index ff68bde3..daf77688 100644 --- a/static/css/graphspace.css +++ b/static/css/graphspace.css @@ -438,4 +438,13 @@ p.lead { position: relative; margin-left: 0; } +} +.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_page.js b/static/js/graphs_page.js index a7f4c57a..a563c2ed 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,24 @@ var apis = { apis.jsonRequest('DELETE', apis.layouts.ENDPOINT({'graph_id': graph_id}) + layout_id, undefined, 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) { @@ -430,6 +457,10 @@ 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. @@ -480,6 +511,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 +1571,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 +1617,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. @@ -1631,6 +2077,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 +2091,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 +2138,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 +2193,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 +2257,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 +2299,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 +2314,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 +2380,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 +2408,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 +2467,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'); + } + }); } } 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..9d14e9f6 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 +313,12 @@

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

    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