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..2805309e 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) @@ -586,3 +596,68 @@ 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): + 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=graph_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 \ No newline at end of file diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 388b8141..dc8833f1 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 '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 @@ -405,7 +414,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 +439,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 +467,92 @@ 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 diff --git a/applications/graphs/models.py b/applications/graphs/models.py index f0e3e554..ad9aeae8 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), '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,51 @@ 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, + 'creator': cls.owner_email, + 'created_at': cls.created_at.isoformat(), + 'updated_at': cls.updated_at.isoformat() + } \ No newline at end of file diff --git a/applications/graphs/tests.py b/applications/graphs/tests.py index f05abfe7..e4330cd2 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,6 +1356,168 @@ 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) + diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 297f92ab..7c59705c 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -27,6 +27,9 @@ # 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'), # REST APIs Endpoints @@ -45,5 +48,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..416dd235 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -114,6 +114,7 @@ def graph_page(request, graph_id): else: return redirect(request.get_full_path() + '?user_layout=' + context["default_layout_id"]) + 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']) @@ -1522,3 +1523,244 @@ 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), + 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) \ No newline at end of file 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/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..1281cf8f 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) { @@ -519,6 +528,32 @@ var graphPage = { export: function (format) { cytoscapeGraph.export(graphPage.cyGraph, format, $('#GraphName').val()); }, + selectGraphVersion: function (row) { + label = $("#version_selector_dropdown").find('a[row_id=' + row + ']').attr('data'); + $("#GraphVersionTable").find('tr').removeClass('success'); + + apis.version.getByID($('#GraphID').val(), row, + successCallback = function (response) { + graph_json = JSON.parse(response.graph_json); + + window.history.pushState('auto-layout', 'Graph Page', window.location.origin + window.location.pathname); + $("#graphVisualizationTabBtn.link-reset").click(); + $(location).attr('href', '#graph_visualization_tab'); + graphPage.init(); + $("#version_selector_dropdown").attr('current_version_id', row); + $("#GraphVersionTable").find('span[row_id=' + row + ']').parent().parent().addClass('success'); + /* Pan and zooms the graph to fit to a collection. */ + window.setTimeout(function () { + graphPage.cyGraph.fit(); + }, 300); + //console.log("Success"); + }, + errorCallback = function (xhr, status, errorThrown) { + // This method is called when error occurs while deleting group_to_graph relationship. + $.notify({message: "You are not authorized to access this Version."}, {type: 'danger'}); + }); + $('#version_selector > bold').text(label); + }, applyAutoLayout: function (layout_id) { graphPage.applyLayout(cytoscapeGraph.getAutomaticLayoutSettings(layout_id)); console.log("after applying layout"); @@ -689,7 +724,9 @@ var graphPage = { onShareGraphWithPublicBtn: function (e, graph_id) { apis.graphs.update(graph_id, { - 'is_public': 1 + 'is_public': 1, + 'version_id': parseInt( $("#version_selector_dropdown").attr('current_version_id')), + 'style_json': style_json //JSON.stringify(style_json) }, successCallback = function (response) { // This method is called when group_to_graph relationship is successfully deleted. @@ -1127,6 +1164,45 @@ var graphPage = { ); } }, + graphVersionTable: { + getVersionByGraphID: function (params) { + /** + * This is the custom ajax request used to load version in graphVersionTable. + * + * params - query parameters for the ajax request. + * It contains parameters like limit, offset, search, sort, order. + */ + + if (params.data["search"]) { + params.data["names"] = _.map(_.filter(_.split(params.data["search"], ','), function (s) { + return s.indexOf(':') === -1; + }), function (str) { + return '%' + str + '%'; + }); + params.data["labels"] = params.data["names"]; + } + + params.data["graph_id"] = $('#GraphID').val(); + params.data["owner_email"] = $('#UserEmail').val(); + apis.version.get($('#GraphID').val(), params.data, + successCallback = function (response) { + // This method is called when nodes are successfully fetched. + params.success(response); + default_version = response.versions.find(x => x.id === default_version_id); + default_version ? $('#current_version_label').text(default_version.name) : $('#current_version_label').text('Default'); + $("#GraphVersionTable").find('span[row_id=' + default_version_id + ']').parent().parent().addClass('success'); + }, + errorCallback = function () { + // This method is called when error occurs while fetching nodes. + params.error('Error'); + } + ); + }, + versionFormatter: function (value, row, index) { + $("#version_selector_dropdown").append('
  • ' + value + '
  • ') + return (''+ value +'') + } + }, layoutsTable: { getPrivateLayoutsByGraphID: function (params) { /** diff --git a/templates/graph/graph_version_table.html b/templates/graph/graph_version_table.html new file mode 100644 index 00000000..2af96c3b --- /dev/null +++ b/templates/graph/graph_version_table.html @@ -0,0 +1,29 @@ + + + + + + {# #} + + {% if uid %} + + {% endif %} + + +
    Version NameDescriptionLast#} + {# Modified#} + {# Created on + Operations +
    diff --git a/templates/graph/index.html b/templates/graph/index.html index 830d44f3..e3d1bde8 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -6,6 +6,7 @@ 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 %} @@ -178,7 +179,7 @@

    Share {% endif %} -
    +
    + +
    @@ -274,6 +289,9 @@

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