From 652195503e50d2ee095ce86b2bbf751a024da63c Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 9 Jun 2018 17:11:27 +0200 Subject: [PATCH 001/105] Added GET Api for Graph Versions --- applications/graphs/controllers.py | 27 ++++ applications/graphs/dal.py | 27 ++++ applications/graphs/models.py | 25 ++++ applications/graphs/urls.py | 6 + applications/graphs/views.py | 162 +++++++++++++++++++++++ static/js/graphs_page.js | 38 ++++++ templates/graph/graph_version_table.html | 36 +++++ templates/graph/index.html | 8 ++ 8 files changed, 329 insertions(+) create mode 100644 templates/graph/graph_version_table.html diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 7f012a05..4e3b96c7 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -586,3 +586,30 @@ 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'): + 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) + + ## TODO: create a util function to relpace the code parse sort and order parameters. This code is repeated again and again. + + 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) \ No newline at end of file diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 388b8141..0efd9e0a 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -458,3 +458,30 @@ 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)): + 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): + return db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() \ No newline at end of file diff --git a/applications/graphs/models.py b/applications/graphs/models.py index f0e3e554..c58a15ba 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -32,6 +32,9 @@ 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") + graph_version = relationship("GraphVersion", foreign_keys="GraphVersion.graph_id", back_populates="graph", + cascade="all, delete-orphan") + groups = association_proxy('shared_with_groups', 'group') tags = association_proxy('graph_tags', 'tag') @@ -279,3 +282,25 @@ 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, unique=True) + graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True) + owner_email = Column(String, ForeignKey('user.email', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + graph_json = Column(String, nullable=False) + description = Column(String, nullable=True) + + graph = relationship("Graph", foreign_keys=[graph_id], back_populates="graph_version", uselist=False) + + def serialize(cls, **kwargs): + 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/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..bed0384d 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1522,3 +1522,165 @@ 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_node(request, graph_id, node=json.loads(request.body))), + content_type="application/json", + status=201) + elif request.method == "DELETE" and version_id is not None: + _delete_node(request, graph_id, version_id) + return HttpResponse(json.dumps({ + "message": "Successfully deleted node 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={}): + """ + + 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. + + Returns + ------- + total : integer + Number of graph versions matching the request. + versions : List of versions. + List of Version Objects with given limit and offset. + + Raises + ------ + + Notes + ------ + + """ + + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) + + querydict = QueryDict('', mutable=True) + querydict.update(query) + query = querydict + + 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) 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)) \ No newline at end of file diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index a7f4c57a..306d0800 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -39,6 +39,12 @@ 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) + }, + }, edges: { ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/edges/'), get: function (graph_id, data, successCallback, errorCallback) { @@ -1127,6 +1133,38 @@ 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(); + + apis.version.get($('#GraphID').val(), params.data, + successCallback = function (response) { + // This method is called when nodes are successfully fetched. + params.success(response); + }, + errorCallback = function () { + // This method is called when error occurs while fetching nodes. + params.error('Error'); + } + ); + } + }, 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..b8a28a3b --- /dev/null +++ b/templates/graph/graph_version_table.html @@ -0,0 +1,36 @@ + +
+ Currently Viewing : +
+ Default Version : +
+ + + + + + + {# #} + + {% if uid %} + + {% endif %} + + +
Version NameDescriptionLast#} + {# Modified#} + {# Created on + Operations +
diff --git a/templates/graph/index.html b/templates/graph/index.html index 830d44f3..50b681c9 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -220,6 +220,11 @@

Layouts {% endif %} + {% if uid %} +
  • + Graph Version +
  • + {% endif %}
    @@ -274,6 +279,9 @@

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

    From 240fd20cf36a945e7bf52f89f5c7af8c0b8a0d0a Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 10 Jun 2018 07:58:26 +0200 Subject: [PATCH 002/105] Added POST Api for graph_version --- applications/graphs/controllers.py | 7 ++++- applications/graphs/dal.py | 8 +++++- applications/graphs/views.py | 45 ++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 4e3b96c7..c877869b 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -612,4 +612,9 @@ def search_graph_versions(request, graph_id=None, names=None, limit=20, offset=0 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) \ No newline at end of file + 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) \ No newline at end of file diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 0efd9e0a..986fec0f 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -484,4 +484,10 @@ def find_graph_versions(db_session, names=None, graph_id=None, limit=None, offse @with_session def get_graph_version_by_id(db_session, id): - return db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() \ No newline at end of file + 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, description=None): + graph_version = GraphVersion(name=name, graph_id=graph_id, graph_json=graph_json, owner_email=owner_email, description=description) + db_session.add(graph_version) + return graph_version \ No newline at end of file diff --git a/applications/graphs/views.py b/applications/graphs/views.py index bed0384d..1e5a5826 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1590,7 +1590,7 @@ def _graph_versions_api(request, graph_id, version_id=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_node(request, graph_id, node=json.loads(request.body))), + 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: @@ -1683,4 +1683,45 @@ def _get_graph_version(request, graph_id, version_id): """ authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) - return utils.serializer(graphs.get_graph_version_by_id(request, version_id)) \ No newline at end of file + 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)) \ No newline at end of file From 64921a35922a435343ea20be6c98aafadcb72e6e Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 10 Jun 2018 08:17:00 +0200 Subject: [PATCH 003/105] Added DELETE API for graph_version --- applications/graphs/controllers.py | 6 +++++- applications/graphs/dal.py | 8 +++++++- applications/graphs/views.py | 32 +++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index c877869b..9169cde4 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -617,4 +617,8 @@ def get_graph_version_by_id(request, 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) \ No newline at end of file + 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 986fec0f..7ca67453 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -490,4 +490,10 @@ def get_graph_version_by_id(db_session, id): def add_graph_version(db_session, graph_id, name, graph_json, owner_email, description=None): graph_version = GraphVersion(name=name, graph_id=graph_id, graph_json=graph_json, owner_email=owner_email, description=description) db_session.add(graph_version) - return graph_version \ No newline at end of file + return graph_version + +@with_session +def delete_graph_version(db_session, id): + graph_version = db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() + db_session.delete(graph_version) + return graph_version \ No newline at end of file diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 1e5a5826..e7b0e94a 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1594,9 +1594,9 @@ def _graph_versions_api(request, graph_id, version_id=None): content_type="application/json", status=201) elif request.method == "DELETE" and version_id is not None: - _delete_node(request, graph_id, version_id) + _delete_graph_version(request, graph_id, version_id) return HttpResponse(json.dumps({ - "message": "Successfully deleted node with id=%s" % (version_id) + "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. @@ -1724,4 +1724,30 @@ def _add_graph_version(request, graph_id, graph_version={}): 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)) \ No newline at end of file + 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 From 4d307e38b5a70ea5bec1fc7d97c1b48d5b7c8f9b Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 10 Jun 2018 14:44:07 +0200 Subject: [PATCH 004/105] Added Version selector dropdown --- applications/graphs/models.py | 28 ++++++++++++++++-------- applications/graphs/views.py | 2 +- static/css/graphspace.css | 9 ++++++++ static/js/graphs_page.js | 25 ++++++++++++++++++++- templates/graph/graph_version_table.html | 8 +++---- templates/graph/index.html | 8 +++++++ 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/applications/graphs/models.py b/applications/graphs/models.py index c58a15ba..3f2121ec 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -295,12 +295,22 @@ class GraphVersion(IDMixin, TimeStampMixin, Base): graph = relationship("Graph", foreign_keys=[graph_id], back_populates="graph_version", uselist=False) def serialize(cls, **kwargs): - 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 + 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/views.py b/applications/graphs/views.py index e7b0e94a..f8ea2609 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1658,7 +1658,7 @@ def _get_graph_versions(request, graph_id, query={}): return { 'total': total, - 'versions': [utils.serializer(version) for version in versions_list] + 'versions': [utils.serializer(version, summary=True) for version in versions_list] } def _get_graph_version(request, graph_id, version_id): 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 306d0800..1f38ccc0 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -44,6 +44,9 @@ var apis = { 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/'), @@ -525,6 +528,22 @@ 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') + + apis.version.getByID($('#GraphID').val(), row, + successCallback = function (response) { + graph_json = JSON.parse(response.graph_json); + graphPage.contructCytoscapeGraph(); + 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); + console.log("abc"); + }, applyAutoLayout: function (layout_id) { graphPage.applyLayout(cytoscapeGraph.getAutomaticLayoutSettings(layout_id)); console.log("after applying layout"); @@ -1163,7 +1182,11 @@ var graphPage = { 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 index b8a28a3b..2e42c9cd 100644 --- a/templates/graph/graph_version_table.html +++ b/templates/graph/graph_version_table.html @@ -13,19 +13,19 @@ data-ajax="graphPage.graphVersionTable.getVersionByGraphID" data-search="true" data-side-pagination="server" - data-data-field="nodes" + data-data-field="versions" data-pagination="true" data-sort-name="name" data-id-field="id" data-sort-order="asc"> - Version Name - Description + Version Name + Description {# Last#} {# Modified#} {# #} - Created on + Created on {% if uid %} Operations diff --git a/templates/graph/index.html b/templates/graph/index.html index 50b681c9..15db3677 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -225,6 +225,14 @@

    Graph Version {% endif %} +
    + + +
    From 321a432e26ff8e930d80c5c9254e136d82753aa4 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Mon, 11 Jun 2018 17:29:33 +0200 Subject: [PATCH 005/105] Added API Specifications for Graph Version --- api_specifications/api.raml | 261 ++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) 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. From f99e6682bbe90f9bba47096178201bb308355dea Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Mon, 18 Jun 2018 18:39:13 +0200 Subject: [PATCH 006/105] Changes in model for version feature --- applications/graphs/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/applications/graphs/models.py b/applications/graphs/models.py index 3f2121ec..2a47d491 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) @@ -290,6 +292,7 @@ class GraphVersion(IDMixin, TimeStampMixin, Base): graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True) 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_version", uselist=False) From 76b4acc39db482b524bec0556ea5dbe260b72cae Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Mon, 18 Jun 2018 22:17:33 +0200 Subject: [PATCH 007/105] Added changes for upload, add graph --- applications/graphs/controllers.py | 14 +++++++++++++- applications/graphs/dal.py | 16 +++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 9169cde4..ab108c76 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -191,8 +191,20 @@ 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) + + # 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) + # Store the index of default version in the graph table (new_graph entry) + new_graph.__setattr__('default_version_id',default_version.id) + db.set_default_version(request.db_session, new_graph.id, default_version.id) + # Add graph tags for tag in G.get_tags(): add_graph_tag(request, new_graph.id, tag) diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 7ca67453..c848ddfe 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -80,8 +80,8 @@ 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, +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 @@ -487,8 +487,8 @@ def get_graph_version_by_id(db_session, id): 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, description=None): - graph_version = GraphVersion(name=name, graph_id=graph_id, graph_json=graph_json, owner_email=owner_email, description=description) +def add_graph_version(db_session, graph_id, name, graph_json, owner_email, style_json=None, description=None, is_default=0): + graph_version = GraphVersion(name=name, graph_id=graph_id, graph_json=graph_json, style_json=style_json, owner_email=owner_email, description=description) db_session.add(graph_version) return graph_version @@ -496,4 +496,10 @@ def add_graph_version(db_session, graph_id, name, graph_json, owner_email, descr def delete_graph_version(db_session, id): graph_version = db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() db_session.delete(graph_version) - return graph_version \ No newline at end of file + return graph_version + +@with_session +def set_default_version(db_session, graph_id, default_version_id): + graph = db_session.query(Graph).filter(Graph.id == graph_id).one_or_none() + setattr(graph, 'default_version_id', default_version_id) + return graph \ No newline at end of file From 50b9c7e2c2bf74bc958d41087bbc7a423242e23a Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:45:18 +0200 Subject: [PATCH 008/105] Update models - migrate json from graph to graph_version table --- applications/graphs/models.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/applications/graphs/models.py b/applications/graphs/models.py index 2a47d491..2e9c73c6 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -34,8 +34,10 @@ 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") - graph_version = relationship("GraphVersion", foreign_keys="GraphVersion.graph_id", 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') @@ -61,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() } @@ -69,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() } @@ -288,14 +292,29 @@ def __table_args__(cls): class GraphVersion(IDMixin, TimeStampMixin, Base): __tablename__ = 'graph_version' - name = Column(String, nullable=False, unique=True) - graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), primary_key=True) + 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'), + ) - graph = relationship("Graph", foreign_keys=[graph_id], back_populates="graph_version", uselist=False) + 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']: From 1cdcb570f2d3ec5b987eea7b897d4d3b822c1084 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:45:52 +0200 Subject: [PATCH 009/105] Added migration files for model changes --- ...bce_update_graph_table_migrate_json_to_.py | 41 ++++++++++++++++++ ...8f6ba9712df_create_graph_versions_table.py | 42 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 migration/versions/840db85c5bce_update_graph_table_migrate_json_to_.py create mode 100644 migration/versions/f8f6ba9712df_create_graph_versions_table.py 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') From e6b59cfd69a25fcecb3ca7a1a69a7b0ef8290f77 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:47:17 +0200 Subject: [PATCH 010/105] Added graph_version specific code to views --- applications/graphs/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/graphs/views.py b/applications/graphs/views.py index f8ea2609..99eece9e 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']) From 9bd157301b45046c5583859720775b0085227844 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:48:34 +0200 Subject: [PATCH 011/105] Added graph_version specific code to controllers --- applications/graphs/controllers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index ab108c76..71383a8d 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -203,7 +203,7 @@ def add_graph(request, name=None, tags=None, is_public=None, graph_json=None, st new_graph.__setattr__('style_json', default_version.style_json) # Store the index of default version in the graph table (new_graph entry) new_graph.__setattr__('default_version_id',default_version.id) - db.set_default_version(request.db_session, new_graph.id, default_version.id) + #db.set_default_version(request.db_session, new_graph.id, default_version.id) # Add graph tags for tag in G.get_tags(): From aed358b4a1fdfdeb6ac0af8a66073b5dbb19d9ef Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:50:39 +0200 Subject: [PATCH 012/105] dal changes - moved json from graph to graph_version --- applications/graphs/dal.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index c848ddfe..2fe311d6 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -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,7 +127,8 @@ 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) + return query.one_or_none() @with_session @@ -130,7 +136,7 @@ def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is 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 +151,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 +165,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 From 9141f5747888c26bf122e2f66ff8976e37274dd0 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 23 Jun 2018 02:52:13 +0200 Subject: [PATCH 013/105] UI changes for graph_version feature --- static/js/graphs_page.js | 20 ++++++++++++++------ templates/graph/graph_version_table.html | 2 -- templates/graph/index.html | 5 +++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index 1f38ccc0..8dea5fd3 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -529,20 +529,23 @@ var graphPage = { cytoscapeGraph.export(graphPage.cyGraph, format, $('#GraphName').val()); }, selectGraphVersion: function (row) { - label = $("#version_selector_dropdown").find('a[row_id=' + row + ']').attr('data') + 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); - graphPage.contructCytoscapeGraph(); - console.log("Success"); + //graphPage.contructCytoscapeGraph(); + graphPage.init(); + $("#version_selector_dropdown").attr('current_version_id', row); + $("#GraphVersionTable").find('span[row_id=' + row + ']').parent().parent().addClass('success'); + //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); - console.log("abc"); }, applyAutoLayout: function (layout_id) { graphPage.applyLayout(cytoscapeGraph.getAutomaticLayoutSettings(layout_id)); @@ -714,7 +717,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. @@ -1176,6 +1181,9 @@ var graphPage = { 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. @@ -1185,7 +1193,7 @@ var graphPage = { }, versionFormatter: function (value, row, index) { $("#version_selector_dropdown").append('
  • ' + value + '
  • ') - return (''+ value +'') + return (''+ value +'') } }, layoutsTable: { diff --git a/templates/graph/graph_version_table.html b/templates/graph/graph_version_table.html index 2e42c9cd..44b89cb7 100644 --- a/templates/graph/graph_version_table.html +++ b/templates/graph/graph_version_table.html @@ -1,7 +1,5 @@
    - Currently Viewing : -
    Default Version :
    diff --git a/templates/graph/index.html b/templates/graph/index.html index 15db3677..4bea8a72 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 %} @@ -228,9 +229,9 @@

    -
    From 5ba4276014360986a8dd8f7af000161b27a31eb3 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 30 Jun 2018 03:39:36 +0200 Subject: [PATCH 014/105] Added Python Documentation for Version Feature --- applications/graphs/controllers.py | 41 +++++++++++--- applications/graphs/dal.py | 85 +++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 71383a8d..2805309e 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -194,16 +194,14 @@ def add_graph(request, name=None, tags=None, is_public=None, graph_json=None, st 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) + 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) - # Store the index of default version in the graph table (new_graph entry) - new_graph.__setattr__('default_version_id',default_version.id) - #db.set_default_version(request.db_session, new_graph.id, default_version.id) # Add graph tags for tag in G.get_tags(): @@ -600,6 +598,37 @@ def delete_edge_by_id(request, 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': @@ -612,8 +641,6 @@ def search_graph_versions(request, graph_id=None, names=None, limit=20, offset=0 else: orber_by = db.asc(sort_attr) - ## TODO: create a util function to relpace the code parse sort and order parameters. This code is repeated again and again. - total, graph_versions = db.find_graph_versions(request.db_session, names=names, graph_id=graph_id, diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 2fe311d6..a3d8eb18 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( @@ -82,7 +82,7 @@ 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, 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) + default_layout_id=default_layout_id) db_session.add(graph) return graph @@ -128,13 +128,14 @@ def delete_graph(db_session, id): @with_session def get_graph_by_id(db_session, id): 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")) @@ -225,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 @@ -322,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: @@ -372,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 @@ -413,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: @@ -438,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: @@ -469,7 +470,18 @@ def find_edges(db_session, is_directed=None, names=None, edges=None, graph_id=No @with_session def find_graph_versions(db_session, names=None, graph_id=None, limit=None, offset=None, - order_by=desc(GraphVersion.updated_at)): + 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: @@ -492,22 +504,55 @@ def find_graph_versions(db_session, names=None, graph_id=None, limit=None, offse @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=0): +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): - graph_version = db_session.query(GraphVersion).filter(GraphVersion.id == id).one_or_none() - db_session.delete(graph_version) - return graph_version + """ + 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 graph \ No newline at end of file + return From 3e1dffcbf2941f3b281ea1c5a740e06b16199a7e Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sat, 30 Jun 2018 03:41:31 +0200 Subject: [PATCH 015/105] Fix indentations and remove unnecessary commented code --- applications/graphs/models.py | 4 ++-- applications/graphs/views.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/applications/graphs/models.py b/applications/graphs/models.py index 2e9c73c6..ad9aeae8 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -300,8 +300,8 @@ class GraphVersion(IDMixin, TimeStampMixin, Base): 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) + 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'), diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 99eece9e..416dd235 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1604,7 +1604,7 @@ def _graph_versions_api(request, graph_id, version_id=None): else: raise BadRequest(request) -def _get_graph_versions(request, graph_id, query={}): +def _get_graph_versions(request, graph_id, query=dict()): """ Query Parameters @@ -1627,6 +1627,8 @@ def _get_graph_versions(request, graph_id, query={}): ---------- request : object HTTP GET Request. + graph_id : string + Unique ID of the graph. Returns ------- @@ -1637,6 +1639,8 @@ def _get_graph_versions(request, graph_id, query={}): 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 ------ @@ -1649,6 +1653,15 @@ def _get_graph_versions(request, graph_id, query={}): 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), @@ -1718,7 +1731,7 @@ def _add_graph_version(request, graph_id, graph_version={}): ------ """ - # + return utils.serializer(graphs.add_graph_version(request, name=graph_version.get('name', None), @@ -1749,6 +1762,5 @@ def _delete_graph_version(request, graph_id, graph_version_id): ------ """ - #authorization.validate(request, permission='GRAPH_UPDATE', graph_id=graph_id) - + 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 From 5f7e22471f6e7b3d6bc6eee25dc35278f7e64036 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 1 Jul 2018 09:30:52 +0200 Subject: [PATCH 016/105] Added Tests for GraphSpace models --- applications/graphs/tests.py | 310 ++++++++++++++++++++++++++--------- graphspace/database.py | 5 + 2 files changed, 241 insertions(+), 74 deletions(-) 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/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) From c2ed1d8f0d7ec439bec0430870cf32f4fa633cff Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Warsi Date: Sun, 1 Jul 2018 09:42:46 +0200 Subject: [PATCH 017/105] Fix indentation --- applications/graphs/dal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index a3d8eb18..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 From 4135efe913f9941d691cf58c02abd1ef09ccbb76 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Mon, 2 Jul 2018 11:14:37 +0200 Subject: [PATCH 018/105] Graph Version minor UI Fixes --- static/js/graphs_page.js | 4 +++- templates/graph/graph_version_table.html | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index 8dea5fd3..b401447d 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -536,6 +536,8 @@ var graphPage = { successCallback = function (response) { graph_json = JSON.parse(response.graph_json); //graphPage.contructCytoscapeGraph(); + $("#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'); @@ -1176,7 +1178,7 @@ var graphPage = { } 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. diff --git a/templates/graph/graph_version_table.html b/templates/graph/graph_version_table.html index 44b89cb7..2af96c3b 100644 --- a/templates/graph/graph_version_table.html +++ b/templates/graph/graph_version_table.html @@ -1,8 +1,3 @@ - -
    - Default Version : -
    - Date: Sun, 8 Jul 2018 15:15:24 +0200 Subject: [PATCH 019/105] Model changes for Layout Compatibility --- applications/graphs/models.py | 26 ++++++++++++- ...4ae3229b_add_table_layout_compatibility.py | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 migration/versions/09f74ae3229b_add_table_layout_compatibility.py diff --git a/applications/graphs/models.py b/applications/graphs/models.py index ad9aeae8..533768d5 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -289,6 +289,7 @@ def __table_args__(cls): args = cls.constraints + cls.indices return args + class GraphVersion(IDMixin, TimeStampMixin, Base): __tablename__ = 'graph_version' @@ -326,7 +327,7 @@ def serialize(cls, **kwargs): 'created_at': cls.created_at.isoformat(), 'updated_at': cls.updated_at.isoformat() } - else : + else: return { 'id': cls.id, 'name': cls.name, @@ -335,4 +336,25 @@ def serialize(cls, **kwargs): 'creator': cls.owner_email, 'created_at': cls.created_at.isoformat(), 'updated_at': cls.updated_at.isoformat() - } \ No newline at end of file + } + + +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/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') From 44f5c26cb1b807e8a1a53acdb22d0193879e900b Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:17:54 +0200 Subject: [PATCH 020/105] Added URLs for Layout Compatibility Feature --- applications/graphs/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 7c59705c..2817f506 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -30,6 +30,8 @@ # 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 From 3053a2785843d07a8306bb2ace6ffec75a2411d7 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:18:42 +0200 Subject: [PATCH 021/105] dal tests for Layout Compatibility --- applications/graphs/tests.py | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/applications/graphs/tests.py b/applications/graphs/tests.py index e4330cd2..ee40488c 100644 --- a/applications/graphs/tests.py +++ b/applications/graphs/tests.py @@ -1519,11 +1519,69 @@ def test_graph_relationship(self): 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() From 2d59317d07d76d208ae60cfa7a2c16cff760ce19 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:21:16 +0200 Subject: [PATCH 022/105] Added templates for Layout Compatibility Tab --- .../graph/layout_compatibility_modal.html | 61 +++++++++++++++++++ .../graph/layout_compatibility_table.html | 21 +++++++ 2 files changed, 82 insertions(+) create mode 100644 templates/graph/layout_compatibility_modal.html create mode 100644 templates/graph/layout_compatibility_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..c7b9015e --- /dev/null +++ b/templates/graph/layout_compatibility_table.html @@ -0,0 +1,21 @@ +
    + + + + + + + +
    Element NameSelector
    From 52cfd74ff71e2de83d9497fc57b6508fda5513e4 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:22:02 +0200 Subject: [PATCH 023/105] Changes in UI for Layout Compatibility Feature --- static/js/graphs_page.js | 179 ++++++++++++++++++++++++++++++++++++- templates/graph/index.html | 9 +- 2 files changed, 184 insertions(+), 4 deletions(-) diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index b401447d..ccea9723 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -72,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) { @@ -439,6 +457,8 @@ var uploadGraphPage = { var graphPage = { cyGraph: undefined, timeout: null, + currentVersionID: null, + conflictingElements: [], init: function () { /** * This function is called to setup the graph page. @@ -538,6 +558,7 @@ var graphPage = { //graphPage.contructCytoscapeGraph(); $("#graphVisualizationTabBtn.link-reset").click(); $(location).attr('href', '#graph_visualization_tab'); + graphPage.currentVersionID = row; graphPage.init(); $("#version_selector_dropdown").attr('current_version_id', row); $("#GraphVersionTable").find('span[row_id=' + row + ']').parent().parent().addClass('success'); @@ -549,6 +570,19 @@ var graphPage = { }); $('#version_selector > bold').text(label); }, + onSelectLayoutCompBtnClick: function (selector) { + elementType = selector.split('[')[0]; + graphPage.cyGraph.elements(selector).select(); + graphPage.layoutEditor.init(); + if (elementType == 'node') { + graphPage.layoutEditor.nodeEditor.open(graphPage.cyGraph.collection(graphPage.cyGraph.nodes(':selected')));//$('#editSelectedNodesBtn').click(); + } + else { + graphPage.layoutEditor.edgeEditor.open(graphPage.cyGraph.collection(graphPage.cyGraph.edges(':selected')));//$('#editSelectedEdgesBtn').click(); + } + $("#graphVisualizationTabBtn.link-reset").click(); + $(location).attr('href', '#graph_visualization_tab'); + }, applyAutoLayout: function (layout_id) { graphPage.applyLayout(cytoscapeGraph.getAutomaticLayoutSettings(layout_id)); console.log("after applying layout"); @@ -575,9 +609,126 @@ var graphPage = { if ($(e).hasClass('auto-layout')) { graphPage.applyAutoLayout($(e).data('layout-id')); } else { - graphPage.applyUserLayout($(e).data('layout-id')); + //graphPage.applyUserLayout($(e).data('layout-id')); + graphPage.getLayoutCompatibilityStatus($(e).data('layout-id')); } }, + getLayoutCompatibilityStatus: function (layout_id) { + //graphPage.applyUserLayout($(e).data('layout-id')); + var params = {data:''}; + params.data["owner_email"] = $('#UserEmail').val(); + apis.compatibility.get($('#GraphID').val(), graphPage.currentVersionID, layout_id, params.data, + successCallback = function (response) { + // This method is called when layouts are successfully fetched. + + if(response==null){ + graphPage.checkLayoutCompatibility(layout_id); + } + else if(response.status=="False"){ + $.notify({ + message: "The selected layout is not compatible with the current version of Graph" + }, { + type: 'danger' + }); + + } + else if(response.status=="True" || response.status=="true"){ + graphPage.applyUserLayout(layout_id); + $("#layout_compatibility_navtab").addClass('invisible'); + } + else { + graphPage.checkLayoutCompatibility(layout_id); + } + }, + errorCallback = function () { + // This method is called when error occurs while fetching layouts. + params.error('Error'); + } + ); + }, + checkLayoutCompatibility: function (layout_id) { + + var layout ={'style_json':'', 'position_json':''}; + var res = null; + var style_json_dict = {}; + var matchedElements = 0; + var compatibility_status = "False"; + var style_json1 = cytoscapeGraph.getStylesheet(graphPage.cyGraph); + graphPage.conflictingElements = []; + $.each(style_json1, function( index, value ) { + style_json_dict[value.selector] = false; + }); + apis.layouts.getByID($('#GraphID').val(), layout_id, + successCallback = function (response) { + layout['style_json'] = JSON.parse(response['style_json']); + layout['positions_json'] = JSON.parse(response['positions_json']); + $.each(layout["style_json"].style, function( index, value ) { + res = graphPage.cyGraph.filter(value.selector); + if (res.length){ + style_json_dict[value.selector]= true; + matchedElements += 1; + } + console.log( index + ": " + value.selector ); + }); + if (Object.keys(style_json_dict).length <= matchedElements) { + compatibility_status = "True"; + graphPage.applyUserLayout(layout_id); + $("#layout_compatibility_navtab").addClass('invisible'); + } + else { + $("#layout_compatibility_navtab").removeClass('invisible'); + $.notify({ + /*message: "Cannot apply " + $('li').find('button[data-layout-id=1] b')[0].textContent + + " for the current version of Graph.
    Could not find style for " + + (Object.keys(style_json_dict).length - matchedElements) + " elements"*/ + message: "" +(Object.keys(style_json_dict).length - matchedElements) + " elements are incompatible with " + + $('li').find('button[data-layout-id=1] b')[0].textContent + " for this Version of the Graph." + + "
    Check Layout Compatibility Tab for details" + }, { + type: 'danger' + }); + } + $.each(style_json1, function( index, value ) { + if (style_json_dict[value.selector] == false){ + graphPage.conflictingElements.push({'name' : value.selector, 'style' : value.style});; + } + }); + //Test + graphPage.applyUserLayout(layout_id); + $('#LayoutCompatibilityTable').bootstrapTable('refresh'); + + apis.compatibility.add($('#GraphID').val(), graphPage.currentVersionID, layout_id, + { + "owner_email": $('#UserEmail').val(), + "graph_id": $('#GraphID').val(), + "compatibility_status": compatibility_status + }, + successCallback = function (response) { + if (Object.keys(style_json_dict).length <= matchedElements){ + /*$.notify({ + message: "Compatibility status of Layout with ID : " + layout_id + " has been updated successfully" + }, { + type: 'success' + });*/ + } + }, + errorCallback = function (response) { + // This method is called when error occurs while deleting group_to_graph relationship. + /*$.notify({ + message: "Error storing Compatibility status" + }, { + type: 'danger' + });*/ + }); + + console.log("Applied Layout on " + matchedElements + " matching elements of "+ Object.keys(style_json_dict).length) + }, + 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 layout, create an account and contact resource's owner for permission to access this layout."}, {type: 'danger'}); + }); + + }, applyLayoutStyle: function (layoutStyle) { layoutStyle = _.map(cytoscapeGraph.parseStylesheet(layoutStyle), function (elemStyle) { elem = graphPage.cyGraph.elements(elemStyle['selector']); @@ -586,13 +737,14 @@ var graphPage = { 'selector': elemStyle['selector'], 'style': graphPage.getGraphSpaceNodeStyle(elemStyle['style'], elem.data()) } - } else { + } else if(elem.isEdge()){ return { 'selector': elemStyle['selector'], 'style': graphPage.getGraphSpaceEdgeStyle(elemStyle['style'], elem.data()) } } }); + layoutStyle = layoutStyle.filter(function(n){ return n != undefined }); graphPage.cyGraph.style().fromJson(_.concat(defaultStylesheet, layoutStyle, selectedElementsStylesheet)).update(); }, applyLayout: function (layoutID) { @@ -1183,6 +1335,7 @@ var graphPage = { successCallback = function (response) { // This method is called when nodes are successfully fetched. params.success(response); + graphPage.currentVersionID = default_version_id; 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'); @@ -1196,7 +1349,27 @@ var graphPage = { versionFormatter: function (value, row, index) { $("#version_selector_dropdown").append('
  • ' + value + '
  • ') return (''+ value +'') - } + }, + }, + layoutCompatibilityTable: { + getConflictingLayoutElements: function (param) { + data = {data : [{'name':'Sample', 'description':'StyleSheet'}]}; + param.success({'data': graphPage.conflictingElements}); + return []; + }, + elementsFormatter: function (value, row, index) { + //$("#version_selector_dropdown").append('
  • ' + value + '
  • ') + value = value.split('\'')[1]; + return (''+ value +'') + }, + selectorFormatter: function (value, row, index) { + //$("#version_selector_dropdown").append('
  • ' + value + '
  • ') + return (''+ value +'') + }, + styleFormatter: function (value, row, index) { + //$("#version_selector_dropdown").append('
  • ' + value + '
  • ') + return (''+ JSON.stringify(value) +'') + }, }, layoutsTable: { getPrivateLayoutsByGraphID: function (params) { diff --git a/templates/graph/index.html b/templates/graph/index.html index 4bea8a72..19cb045a 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -226,6 +226,11 @@

    Graph Version {% endif %} + {% if uid %} + + {% endif %}

    From 246b83c5509aa171cef200a5ec165fb94e61b63a Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:23:10 +0200 Subject: [PATCH 024/105] Added CRUD methods for server Layout Compatibility AJAX Requests --- applications/graphs/views.py | 200 ++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 416dd235..9c5152eb 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -1604,6 +1604,7 @@ def _graph_versions_api(request, graph_id, version_id=None): else: raise BadRequest(request) + def _get_graph_versions(request, graph_id, query=dict()): """ @@ -1675,6 +1676,7 @@ def _get_graph_versions(request, graph_id, query=dict()): 'versions': [utils.serializer(version, summary=True) for version in versions_list] } + def _get_graph_version(request, graph_id, version_id): """ @@ -1699,6 +1701,7 @@ def _get_graph_version(request, graph_id, version_id): 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={}): """ @@ -1740,6 +1743,7 @@ def _add_graph_version(request, graph_id, graph_version={}): graph_json=graph_version.get('graph_json', None), graph_id=graph_id)) + @is_authenticated() def _delete_graph_version(request, graph_id, graph_version_id): """ @@ -1763,4 +1767,198 @@ def _delete_graph_version(request, graph_id, graph_version_id): """ 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 + 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) From c6d2dd585006fcc685d16d933ed718aa7245a9d2 Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Sun, 8 Jul 2018 15:24:30 +0200 Subject: [PATCH 025/105] Added scripts for Database interactions for Layout Compatibility --- applications/graphs/controllers.py | 24 ++++++++++- applications/graphs/dal.py | 68 ++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 2805309e..18eae9d8 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -650,14 +650,36 @@ def search_graph_versions(request, graph_id=None, names=None, limit=20, offset=0 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 + 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 dc8833f1..eef2ccc8 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -396,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 @@ -556,3 +559,68 @@ def set_default_version(db_session, graph_id, default_version_id): 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] From b2aeab286c882225682ea47c3d590d820c91294c Mon Sep 17 00:00:00 2001 From: Daniyal Jahan Date: Thu, 2 Aug 2018 16:01:17 +0200 Subject: [PATCH 026/105] Update UI components in the templates --- templates/graph/graph_version_table.html | 8 +- templates/graph/index.html | 26 +- .../graph/layout_compatibility_table.html | 259 ++++++++++++++++-- 3 files changed, 265 insertions(+), 28 deletions(-) diff --git a/templates/graph/graph_version_table.html b/templates/graph/graph_version_table.html index 2af96c3b..a3489523 100644 --- a/templates/graph/graph_version_table.html +++ b/templates/graph/graph_version_table.html @@ -18,12 +18,12 @@ {# Last#} {# Modified#} {# #} - Created on - {% if uid %} - + Created at + + Operations - {% endif %} + diff --git a/templates/graph/index.html b/templates/graph/index.html index 19cb045a..9d14e9f6 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -13,6 +13,7 @@
    {% include 'graph/delete_layout_modal.html' %} + {% include 'graph/layout_compatibility_table.html' %} {% include 'graph/edit_layout_modal.html' %} +
    + + +
    + + +
    diff --git a/templates/compare_graph/compare_modal.html b/templates/compare_graph/compare_modal.html new file mode 100644 index 00000000..44366960 --- /dev/null +++ b/templates/compare_graph/compare_modal.html @@ -0,0 +1,142 @@ + \ No newline at end of file From 1dcaddedeb658324632ce5e5ca88d45c06010c37 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Wed, 31 Jul 2019 11:30:17 +0200 Subject: [PATCH 080/105] Update view for N-graph comparison --- applications/graphs/views.py | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/applications/graphs/views.py b/applications/graphs/views.py index ff69c0c2..0ca1fc66 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -15,6 +15,26 @@ def compare_graph_page(request): + """ + Wrapper view function for the following pages: + /compare/ + + Parameters + ---------- + request : HTTP Request + + Returns + ------- + response : HTML Page Response + Rendered graphs list page in HTML. + + Raises + ------ + MethodNotAllowed: If a user tries to send requests other than GET i.e., POST, PUT or UPDATE. + + Notes + ------ + """ context = RequestContext(request, {}) if request.GET.get('graph_1') is None and request.GET.get('graph_2') is None \ and request.GET.get('operation') is None: @@ -32,10 +52,33 @@ def compare_graph_page(request): def compare_graphs(request): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + + Returns + ------- + JSON Response of nodes & edges + + Raises + ------ + MethodNotAllowed: If a user tries to send requests other than GET i.e., POST, PUT or UPDATE. + + Notes + ------ + + """ context = RequestContext(request, {}) if request.META.get('HTTP_ACCEPT', None) == 'application/json': if request.method == "GET": + if request.GET.get('multiple'): + return HttpResponse(json.dumps(_compare_graph_multiple(request, request.GET)), + content_type="application/json", status=200) + return HttpResponse(json.dumps(_compare_graph(request, request.GET['graph_1_id'], request.GET['graph_2_id'], request.GET['operation'])), content_type="application/json", status=200) @@ -45,6 +88,43 @@ def compare_graphs(request): raise MethodNotAllowed(request) +def _compare_graph_multiple(request, data): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + data : dict + graph_ids and operation. Required. + + Returns + ------- + nodes & edges: object + + Raises + ------ + + Notes + ------ + + """ + + # authorization.validate(request, permission='GRAPH_READ', graph_id=graph_1_id) + # authorization.validate(request, permission='GRAPH_READ', graph_id=graph_2_id)` + + # Populate list of graph_ids + id_list = [] + for i in range(1, len(data)-1): + if data.get('graph_id_' + str(i)): + id_list.append(data.get('graph_id_' + str(i))) + + nodes, edges = graphs.get_graph_comparison_multi(request, id_list, data.get('operation')) + edges = [utils.serializer(edge) for edge in edges[1]] + nodes = [utils.serializer(node) for node in nodes[1]] + return {'edges': edges, 'nodes': nodes} + + def _compare_graph(request, graph_1_id, graph_2_id, operation): """ From 05f2dc1fa8c1fc13c312c9d0edc0e03ebec06943 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Wed, 31 Jul 2019 11:43:47 +0200 Subject: [PATCH 081/105] Update controller for N-graph comparison --- applications/graphs/controllers.py | 116 ++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index c8dab3ad..720599e4 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -589,6 +589,23 @@ def delete_edge_by_id(request, edge_id): def get_graph_comparison(request, graph_1, graph_2, operation): + """ + Caller function to execute 2-Graph Intersection or Graph Difference operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ if operation == 'intersection': return get_graphs_intersection(request, graph_1, graph_2) else: @@ -596,15 +613,110 @@ def get_graph_comparison(request, graph_1, graph_2, operation): def get_graphs_intersection(request, graph_1, graph_2): - # calling nodes_comparison function for testing purpose only - # db.nodes_comparison(request.db_session,) + """ + Caller function to execute 2-Graph Intersection operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + """ node_data = db.nodes_intersection(request.db_session, graph_1, graph_2) edge_data = db.edges_intersection(request.db_session, graph_1, graph_2) return node_data, edge_data def get_graphs_difference(request, graph_1, graph_2): + """ + Caller function to execute 2-Graph Difference operation. + + Parameters + ---------- + graph_1: string + Unique ID of the 1st graph. Required. + graph_2: string + Unique ID of the 1st graph. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_difference(request.db_session, graph_1, graph_2) + edge_data = db.edges_difference(request.db_session, graph_1, graph_2) + return node_data, edge_data + + +def get_graph_comparison_multi(request, graphs, operation): + """ + Caller function to execute N-Graph Intersection or Graph Difference operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + if operation == 'intersection': + return get_graphs_intersection_multi(request, graphs) + else: + return get_graphs_difference_multi(request, graphs) + + +def get_graphs_intersection_multi(request, graphs): + """ + Caller function to execute N-Graph Intersection operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ + node_data = db.nodes_intersection_multi(request.db_session, graphs) + edge_data = db.edges_intersection_multi(request.db_session, graphs) + return node_data, edge_data + + +def get_graphs_difference_multi(request, graphs): + """ + Caller function to execute N-Graph Difference operation. + + Parameters + ---------- + graphs: List[string] + Unique IDs of the graphs. Required. + operation: string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + """ node_data = db.nodes_difference(request.db_session, graph_1, graph_2) edge_data = db.edges_difference(request.db_session, graph_1, graph_2) return node_data, edge_data From 4e6819971ee64254ff9d1a48fe8be7eeacd0d104 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Wed, 31 Jul 2019 11:45:05 +0200 Subject: [PATCH 082/105] Add API endpoint for N-graph comparison --- applications/graphs/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 3c5ab587..60862ddd 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -17,6 +17,7 @@ url(r'^ajax/graphs/advanced_search$', views.graphs_advanced_search_ajax_api, name='graphs_advanced_search_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)$', views.graphs_ajax_api, name='graph_ajax_api'), url(r'^ajax/compare/$', views.compare_graphs, name='compare_graph'), + url(r'^ajax/compare/multi/$', views.compare_graphs, name='compare_graph'), # Graphs Groups url(r'^ajax/graphs/(?P[^/]+)/groups$', views.graph_groups_ajax_api, name='graph_groups_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)/groups/(?P[^/]+)$', views.graph_groups_ajax_api, name='graph_group_ajax_api'), From 328e9b7a5001b5fa40dcafc7292fbfe5647a9da5 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Wed, 31 Jul 2019 11:58:08 +0200 Subject: [PATCH 083/105] Add functions to support N-Graph comparison --- applications/graphs/dal.py | 102 ++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 46164eb3..be9c6a6c 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -462,6 +462,13 @@ def find_edges(db_session, is_directed=None, names=None, edges=None, graph_id=No @with_session def nodes_intersection(db_session, graph_1_id=None, graph_2_id=None): + """ + Find node intersection of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ alias_node = aliased(Node) query = db_session.query(Node, alias_node) @@ -475,6 +482,13 @@ def nodes_intersection(db_session, graph_1_id=None, graph_2_id=None): @with_session def nodes_difference(db_session, graph_1_id=None, graph_2_id=None): + """ + Find node difference of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) sub_q = graph2_query.subquery() @@ -487,6 +501,13 @@ def nodes_difference(db_session, graph_1_id=None, graph_2_id=None): @with_session def edges_intersection(db_session, graph_1_id=None, graph_2_id=None): + """ + Find edge intersection of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ alias_edge = aliased(Edge) query = db_session.query(Edge, alias_edge) @@ -501,6 +522,13 @@ def edges_intersection(db_session, graph_1_id=None, graph_2_id=None): @with_session def edges_difference(db_session, graph_1_id=None, graph_2_id=None): + """ + Find edge difference of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :return: list: Total, queried nodes + """ graph1_query = db_session.query(Edge).filter(Edge.graph_id == graph_1_id) graph2_query = db_session.query(Edge).filter(Edge.graph_id == graph_2_id) sub_q = graph2_query.subquery() @@ -513,14 +541,25 @@ def edges_difference(db_session, graph_1_id=None, graph_2_id=None): @with_session def nodes_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): - alias_node = aliased(Node) - if operation == 'i': - query = db_session.query(Node, alias_node) + """ + Find node subquery of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :param operation: Comparison operation - difference or intersection.. + :return: query.subquery object. + """ + alias_node1 = aliased(Node, name='n1') + alias_node2 = aliased(Node, name='n2') + if operation == 'intersection': + query = db_session.query(alias_node1, alias_node2) if graph_1_id is not None and graph_2_id is not None: - query = query.filter(Node.graph_id == graph_1_id). \ - join(alias_node, Node.name == alias_node.name). \ - filter(alias_node.graph_id == graph_2_id) - return query.subquery() + query = query.filter(alias_node1.graph_id == graph_1_id). \ + join(alias_node2, alias_node1.name == alias_node2.name). \ + filter(alias_node2.graph_id == graph_2_id) + n1 = query.subquery(name='s1', with_labels=True) + db_session.query(Node).filter(Node.graph_id == 6).join(n1, n1.c.name == Node.name) + return query.subquery(name='s1', with_labels=True) else: graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) @@ -532,6 +571,14 @@ def nodes_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None) @with_session def edges_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): + """ + Find node subquery of 2 Graphs. + :param db_session: Database session. + :param graph_1_id: Unique ID of Graph 1. + :param graph_2_id: Unique ID of the Graph 2. + :param operation: Comparison operation - difference or intersection.. + :return: query.subquery object. + """ alias_edge = aliased(Edge) if operation == 'intersection': query = db_session.query(Edge, alias_edge) @@ -552,8 +599,47 @@ def edges_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None) return query.subquery +@with_session +def nodes_intersection_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Node).filter(Node.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Node).filter(Node.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.name == Node.name) + total = query.count() + return total, query.all() + + +@with_session +def edges_intersection_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Edge).filter(Edge.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Edge).filter(Edge.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.head_node_name == Edge.head_node_name)\ + .filter(Edge.tail_node_name == sub_q.c.tail_node_name) + total = query.count() + return total, query.all() + + @with_session def nodes_comparison(db_session, comp_expression=None): + """ + Work In Progress. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ # Infix -> a 'i' ( b - c ) # Postfix -> a b c - 'i' @@ -576,3 +662,5 @@ def nodes_comparison(db_session, comp_expression=None): count = query.count() return count, query.all() + + From f6fc00861529ac006ce4189fe7c6750197e911c6 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Wed, 31 Jul 2019 13:56:30 +0200 Subject: [PATCH 084/105] Add reset function for menus --- static/js/graphs_compare.js | 22 ++++++++++++++++++++++ templates/compare_graph/compare_modal.html | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js index a12617a0..269d5fc2 100644 --- a/static/js/graphs_compare.js +++ b/static/js/graphs_compare.js @@ -26,6 +26,9 @@ var compareGraphPage = { }, 100); compareGraphPage.cyGraph.fit().center(); }); + $('#resetMenus').click(function () { + compareGraphPage.resetMenus(); + }); $("#search-place-holder").on("keyup", function () { var value = $(this).val().toLowerCase(); $(".dropdown-menu li").filter(function () { @@ -113,6 +116,25 @@ var compareGraphPage = { compareGraphPage.setNodesColor('common_1', common.val()); } }, + resetMenus: function(){ + /** + * This function is called to whenever user wants to reset selection. + * All dropdown menus are reset to default state. + * graph_ids need to reset to allow fresh graph comparison + */ + compareGraphPage.graph_ids= []; + for (let i = 1; i<8; i++){ + $('#dropdownMenu'+i).attr('value', undefined); + $('#dropdownMenu'+i).val(''); + $('#dropdownMenu'+i).children().text(''); + $('#dropdownMenu'+i+' > i').text('Select Graph '+i); + if (i<3){ + $('#operatorMenu'+i).attr('value', undefined); + $('#operatorMenu'+i).text('Select Operation'); + $('#operatorMenu'+i).append(''); + } + } + }, validateExpression: function (infix) { var balance = 0; // remove white spaces to simplify regex diff --git a/templates/compare_graph/compare_modal.html b/templates/compare_graph/compare_modal.html index 44366960..1d267a8c 100644 --- a/templates/compare_graph/compare_modal.html +++ b/templates/compare_graph/compare_modal.html @@ -132,7 +132,7 @@

    - +
    From 3d0040b079dde2167cd4f2cad9f11aa71d197c98 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Mon, 5 Aug 2019 12:30:52 +0200 Subject: [PATCH 085/105] Add classes to cygraph --- static/js/graphs_compare.js | 96 +++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js index 269d5fc2..9cc23f8f 100644 --- a/static/js/graphs_compare.js +++ b/static/js/graphs_compare.js @@ -8,6 +8,7 @@ var compareGraphPage = { graphs_json: [], styles_json: [], common_nodes: undefined, + common_metadata: {}, edge_name_to_id: {}, timeout: null, init: function () { @@ -395,6 +396,52 @@ var compareGraphPage = { // } graph_json['elements']['nodes'][len - duplicate_nodes.length]['data']['parent'] = 'graph_2'; }, + setGraphClasses: function(graph_json, common_nodes, graph_id){ + /** + * Params + * graph_json : nodes & edges datastructure + * common_nodes : nodes indentified by the comparison function + * graph_id : identity of graph + * + * This function assigns a group to elements of a graph based on its ID. + * This is required later to identify which graph nodes/edges belong to + * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs + * to be merged into a single graph grouping helps in identifying elements + * of individual graphs). + * Also, removes duplicate entries for common_nodes from the merged graph. + * + * It is called in compareGraphHelperMultiple() during comparison operation. + */ + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + var duplicate_nodes = []; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != (graph_id + 1)) + item['classes'] = 'graph_' + (graph_id + 1); + _.each(common_nodes, function (innerNode) { + if (!compareGraphPage.common_metadata[innerNode['name']]) + compareGraphPage.common_metadata[innerNode['name']] = []; + if (item['data']['name'] == innerNode['name']) { + let g_id = compareGraphPage.graph_ids[graph_id]; + if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) + duplicate_nodes.push(item); + if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { + if (!common_id) { + common_id = item['data']['id']; + } + item['classes'] = 'common_1'; + } + compareGraphPage.common_metadata[innerNode['name']].push(item['data']); + } + }); + }); + // if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) { + _.each(duplicate_nodes, function (node) { + graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); + }); + // } + // graph_json['elements']['nodes'][len - duplicate_nodes.length]['data']['parent'] = (graph_id + 1); + }, setGraphGroupsMultiple: function (graph_json, common_nodes, graph_id) { /** * Params @@ -425,7 +472,10 @@ var compareGraphPage = { if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; _.each(common_nodes, function (innerNode) { + if (!compareGraphPage.common_metadata[innerNode['name']]) + compareGraphPage.common_metadata[innerNode['name']] = []; if (item['data']['name'] == innerNode['name']) { + let g_id = compareGraphPage.graph_ids[graph_id]; if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) duplicate_nodes.push(item); if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { @@ -435,6 +485,7 @@ var compareGraphPage = { } item['data']['parent'] = 'common_1'; } + compareGraphPage.common_metadata[innerNode['name']].push(item['data']); } }); }); @@ -616,6 +667,7 @@ var compareGraphPage = { console.log('Success'); compareGraphPage.common_nodes = response['nodes']; for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { + compareGraphPage.setGraphClasses(compareGraphPage.graphs_json[i], response['nodes'], i); compareGraphPage.setGraphGroupsMultiple(compareGraphPage.graphs_json[i], response['nodes'], i); if (i > 0) { compareGraphPage.graphs_json[0]['elements']['nodes'] = @@ -643,12 +695,12 @@ var compareGraphPage = { }, 100); }); - compareGraphPage.populateNodeDataMulti(response['nodes']); + compareGraphPage.populateNodeData(response['nodes']); compareGraphPage.populateEdgeData(response['edges']); $('#nodes-total-badge').text(response['nodes'].length); $('#edges-total-badge').text(response['edges'].length); - compareGraphPage.tabHelper(); + // compareGraphPage.tabHelper(); }, errorCallback = function (xhr, status, errorThrown) { @@ -765,7 +817,6 @@ var compareGraphPage = { } var trHTML = ''; - var cyNode = undefined; $('#nodes-table').DataTable().clear().destroy(); $('#nodes-comparison-table').find("tr:gt(0)").remove(); if (nodes.length && !nodes[0].length) { @@ -774,38 +825,20 @@ var compareGraphPage = { $('#nodes-table').attr('style', 'width:800px;'); } else $('#nodes-table').attr('style', ''); - $.each(nodes, function (i, item) { + $.each(compareGraphPage.common_metadata, function (i, item) { if (item.length) { // Use 'name' field instead - for testing use 'label' - cyNode = compareGraphPage.cyGraph.nodes("[label = '" + item['label'] + "']"); - for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { - - trHTML += 'Name : ' + item['name'] - + '
    Label : ' + item['label']; - if (cyNode.length && cyNode.data() && cyNode.data()['popup']) { - trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); - } - - trHTML += ' Name : ' + item[1]['name'] - + '
    Label : ' + item[1]['label']; + trHTML += '' + $.each(item, function (j, node) { - if (cyNode2.length && cyNode2.data() && cyNode2.data()['popup']) { - trHTML += '
    ' + cyNode2.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + trHTML += 'Name : ' + node['name'] + + '
    Label : ' + node['label']; + if (node['popup']) { + trHTML += '
    ' + node['popup'].replace(/<\s*hr\s*\/>/gi, ''); } - trHTML += ''; - } - - } else { - // Use 'name' field instead - for testing use 'label' - cyNode1 = compareGraphPage.cyGraph.getElementById(item['label']); - - trHTML += 'Name : ' + item['name'] - + '
    Label : ' + item['label']; - - if (cyNode1.length && cyNode1.data() && cyNode1.data()['popup']) { - trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); - } - trHTML += ''; + trHTML += ''; + }); + trHTML += ''; } }); @@ -938,6 +971,7 @@ var compareGraphPage = { }; if ('position' in node) { newNode['position'] = node['position']; + newNode['classes'] = node['classes'] if (node['data']['parent'] == 'graph_1') { x_max = (x_max > node['position']['x']) ? x_max : node['position']['x']; } From 84d286be930cc10511abb7dedeab2c43c8c29939 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 6 Aug 2019 12:12:43 +0200 Subject: [PATCH 086/105] Support graph comparison from index page --- static/js/graphs_compare.js | 168 +++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 79 deletions(-) diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js index 9cc23f8f..1be3679a 100644 --- a/static/js/graphs_compare.js +++ b/static/js/graphs_compare.js @@ -21,12 +21,13 @@ var compareGraphPage = { $('#edges-li').hide(); $('#visualization-li').hide(); compareGraphPage.loadGraphs(); - $('#graphVisualizationTabBtn').click(function (e) { + /*$('#graphVisualizationTabBtn').click(function (e) { window.setTimeout(function () { $('#cyGraphContainer').css('height', '99%'); + compareGraphPage.cyGraph.fit().center(); }, 100); - compareGraphPage.cyGraph.fit().center(); - }); + + });*/ $('#resetMenus').click(function () { compareGraphPage.resetMenus(); }); @@ -52,12 +53,6 @@ var compareGraphPage = { }); $('#colorpicker1').colorpicker(); $('#colorpicker1').colorpicker().on('changeColor', function (event) { - // $('#colorpicker1').css('background-color', event.color.toString()); - // $('#colorpicker1').val(event.color.toString()); - // if (compareGraphPage.cyGraph) { - // compareGraphPage.setNodesColor('graph_1', event.color.toString()); - // compareGraphPage.setNodesColor('common_1', $('#operatorcolorpicker').val()); - // } compareGraphPage.colorPickerHelper($('#colorpicker1'), event, 'graph_1', $('#operatorcolorpicker')); }); $('#colorpicker2').colorpicker(); @@ -103,7 +98,40 @@ var compareGraphPage = { $('#operatorMenu1').attr('value', operation); $('#operatorMenu1').parent().find('a[row_id="' + operation + '"]').click(); } - + if (graph_ids && operation) { + for (let i = 0; i < graph_ids.length; i++) { + $('#dropdownMenu' + (i + 3)).attr('value', graph_ids[i]); + } + $('#operatorMenu2').attr('value', operation); + $('#operatorMenu2').parent().find('a[row_id="' + operation + '"]').click(); + } + }, + selectGraphForCompare: function () { + /** + * This function is called to when User selects + * graphs for comparison from Graph Index Page. + * It generates the graph comparison url for the selected graphs. + */ + let href = '/compare?' + _.each($('input:checkbox:checked'), function (item) { + href += 'id=' + item.getAttribute('row_id') + '&'; + }); + if ($('input:checkbox:checked').length > 1) { + console.log('Compare'); + $('#comparehref > li > a')[0].setAttribute('href', href + 'operation=intersection'); + $('#comparehref > li > a')[1].setAttribute('href', href + 'operation=difference'); + $('#compareHrefDiv').css('visibility', 'visible'); + } else { + $('#compareHrefDiv').css('visibility', 'hidden'); + } + if ($('input:checkbox:checked').length == 2) { + console.log('Compare'); + $('#comparehref > li > a')[0].setAttribute('href', '/compare?graph_1=' + $('input:checkbox:checked')[0].getAttribute('row_id') + + '&graph_2=' + $('input:checkbox:checked')[1].getAttribute('row_id') + '&operation=intersection'); + $('#comparehref > li > a')[1].setAttribute('href', '/compare?graph_1=' + $('input:checkbox:checked')[0].getAttribute('row_id') + + '&graph_2=' + $('input:checkbox:checked')[1].getAttribute('row_id') + '&operation=difference'); + $('#compareHrefDiv').css('visibility', 'visible'); + } }, colorPickerHelper: function (obj, event, graph_id, common) { /** @@ -117,26 +145,30 @@ var compareGraphPage = { compareGraphPage.setNodesColor('common_1', common.val()); } }, - resetMenus: function(){ + resetMenus: function () { /** * This function is called to whenever user wants to reset selection. * All dropdown menus are reset to default state. * graph_ids need to reset to allow fresh graph comparison */ - compareGraphPage.graph_ids= []; - for (let i = 1; i<8; i++){ - $('#dropdownMenu'+i).attr('value', undefined); - $('#dropdownMenu'+i).val(''); - $('#dropdownMenu'+i).children().text(''); - $('#dropdownMenu'+i+' > i').text('Select Graph '+i); - if (i<3){ - $('#operatorMenu'+i).attr('value', undefined); - $('#operatorMenu'+i).text('Select Operation'); - $('#operatorMenu'+i).append(''); + compareGraphPage.graph_ids = []; + for (let i = 1; i < 8; i++) { + $('#dropdownMenu' + i).attr('value', undefined); + $('#dropdownMenu' + i).val(''); + $('#dropdownMenu' + i).children().text(''); + $('#dropdownMenu' + i + ' > i').text('Select Graph ' + i); + if (i < 3) { + $('#operatorMenu' + i).attr('value', undefined); + $('#operatorMenu' + i).text('Select Operation'); + $('#operatorMenu' + i).append(''); } } }, validateExpression: function (infix) { + /** + * Might be needed in future! + * + */ var balance = 0; // remove white spaces to simplify regex infix = infix.replace(/ /g, ''); @@ -264,24 +296,15 @@ var compareGraphPage = { * graph_parent : identifies the group of nodes * color : color for the group of nodes * - * This function color a group of nodes according to the graph_parent id. + * This function color a group of nodes according to the class of the element. * It is called when comparison is executed and whenever user changes node * color dynamically using the colorpickers. */ - compareGraphPage.cyGraph.filter(":parent[id='" + graph_parent + "']").style({ - 'background-color': color, - 'background-opacity': 0, - 'border-opacity': 0, - 'border-color': color, - }); - compareGraphPage.cyGraph.filter("node[parent='" + graph_parent + "']").style({ + compareGraphPage.cyGraph.$("." + graph_parent).style({ 'background-color': color, 'border-color': color }); - compareGraphPage.cyGraph.filter("node[parent='" + graph_parent + "']").connectedEdges().style({'line-color': color}); - compareGraphPage.cyGraph.filter("node[id='graph_1']").style({'background-opacity': 0, 'font-size': '1px'}); - compareGraphPage.cyGraph.filter("node[id='graph_2']").style({'background-opacity': 0, 'font-size': '1px'}); - compareGraphPage.cyGraph.filter("node[id='common_1']").style({'background-opacity': 0, 'font-size': '1px'}); + compareGraphPage.cyGraph.$("." + graph_parent).connectedEdges().style({'line-color': color}); }, setNodesColorMultiple: function (graph_parent, color) { /** @@ -293,25 +316,25 @@ var compareGraphPage = { * It is called when comparison is executed and whenever user changes node * color dynamically using the colorpickers. */ - compareGraphPage.cyGraph.filter(":parent[id='" + graph_parent + "']").style({ + compareGraphPage.cyGraph.$("." + graph_parent).style({ 'background-color': color, 'background-opacity': 0, 'border-opacity': 0, 'border-color': color, }); - compareGraphPage.cyGraph.filter("node[parent='" + graph_parent + "']").style({ + compareGraphPage.cyGraph.$("." + graph_parent).style({ 'background-color': color, 'border-color': color }); - compareGraphPage.cyGraph.filter("node[parent='" + graph_parent + "']").connectedEdges().style({'line-color': color}); + compareGraphPage.cyGraph.$("." + graph_parent).connectedEdges().style({'line-color': color}); for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { - compareGraphPage.cyGraph.filter("node[id='graph_'" + (i + 1) + "]").style({ + compareGraphPage.cyGraph.$(".graph_" + (i + 1)).style({ 'background-opacity': 0, 'font-size': '1px' }); } - compareGraphPage.cyGraph.filter("node[id='common_1']").style({'background-opacity': 0, 'font-size': '1px'}); + compareGraphPage.cyGraph.$('.common_1').style({'background-opacity': 0, 'font-size': '1px'}); }, setGraph0Groups: function (graph_json, common_nodes) { /** @@ -351,32 +374,24 @@ var compareGraphPage = { * common_nodes : nodes indentified by the comparison function * graph_id : identity of graph * - * This function assigns a group to elements of a graph based on its ID. - * This is required later to identify which graph nodes/edges belong to + * This function assigns classes to elements of a graph based on its ID. + * This is required later to identify nodes/edges for each graphs. * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs - * to be merged into a single graph grouping helps in identifying elements + * to be merged into a single graph. Classes help in identifying elements * of individual graphs). * Also, removes duplicate entries for common_nodes from the merged graph. * * It is called in compareGraphHelper() during comparison operation. */ - const len = graph_json['elements']['nodes'].length; var common_id = undefined; var duplicate_nodes = []; - graph_json['elements']['nodes'][len] = { - 'data': { - 'id': 'graph_' + (graph_id + 1), - 'label': 'Graph ' + (graph_id + 1), - 'name': 'Graph ' + (graph_id + 1) - } - }; _.each(graph_json['elements']['nodes'], function (item) { - if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) - item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; + if (item['data']['id'] != (graph_id + 1)) + item['classes'] = 'graph_' + (graph_id + 1); _.each(common_nodes, function (innerNode) { if (innerNode.length) innerNode = (innerNode[0]['graph_id'] == compareGraphPage.graph_ids[graph_id]) ? innerNode[0] : innerNode[1]; - if (item['data']['label'] == innerNode['label']) { + if (item['data']['name'] == innerNode['name'] || item['data']['label'] == innerNode['label']) { if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) duplicate_nodes.push(item); if (compareGraphPage.graph_ids[graph_id] == compareGraphPage.graph_ids[0]) { @@ -384,29 +399,26 @@ var compareGraphPage = { common_id = item['data']['id']; compareGraphPage.common_nodes['parent1'] = common_id; } - item['data']['parent'] = 'common_1'; + item['classes'] = 'common_1'; } } }); }); - // if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) { _.each(duplicate_nodes, function (node) { graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); }); - // } - graph_json['elements']['nodes'][len - duplicate_nodes.length]['data']['parent'] = 'graph_2'; }, - setGraphClasses: function(graph_json, common_nodes, graph_id){ + setGraphClasses: function (graph_json, common_nodes, graph_id) { /** * Params * graph_json : nodes & edges datastructure * common_nodes : nodes indentified by the comparison function * graph_id : identity of graph * - * This function assigns a group to elements of a graph based on its ID. - * This is required later to identify which graph nodes/edges belong to + * This function assigns a classes to elements of a graph based on its ID. + * This is required later to identify nodes/edges for each graphs. * (as Cytoscape.js only supports 1 instance per canvas - all graphs needs - * to be merged into a single graph grouping helps in identifying elements + * to be merged into a single graph. Classes help in identifying elements * of individual graphs). * Also, removes duplicate entries for common_nodes from the merged graph. * @@ -421,7 +433,7 @@ var compareGraphPage = { _.each(common_nodes, function (innerNode) { if (!compareGraphPage.common_metadata[innerNode['name']]) compareGraphPage.common_metadata[innerNode['name']] = []; - if (item['data']['name'] == innerNode['name']) { + if (item['data']['name'] == innerNode['name'] || item['data']['label'] == innerNode['label']) { let g_id = compareGraphPage.graph_ids[graph_id]; if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) duplicate_nodes.push(item); @@ -435,12 +447,9 @@ var compareGraphPage = { } }); }); - // if (compareGraphPage.graph_ids[graph_id] != compareGraphPage.graph_ids[0]) { _.each(duplicate_nodes, function (node) { graph_json['elements']['nodes'].splice(graph_json['elements']['nodes'].indexOf(node), 1); }); - // } - // graph_json['elements']['nodes'][len - duplicate_nodes.length]['data']['parent'] = (graph_id + 1); }, setGraphGroupsMultiple: function (graph_json, common_nodes, graph_id) { /** @@ -543,7 +552,7 @@ var compareGraphPage = { */ graph_1_id = compareGraphPage.graph_ids[0] = $('#dropdownMenu1').attr('value'); graph_2_id = compareGraphPage.graph_ids[1] = $('#dropdownMenu2').attr('value'); - + $('#visualization-li a:first').tab('show'); operation = $('#operatorMenu1').attr('value'); apis.graphs.getByID(compareGraphPage.graph_ids[0], successCallback = function (response) { @@ -668,7 +677,7 @@ var compareGraphPage = { compareGraphPage.common_nodes = response['nodes']; for (let i = 0; i < compareGraphPage.graph_ids.length; i++) { compareGraphPage.setGraphClasses(compareGraphPage.graphs_json[i], response['nodes'], i); - compareGraphPage.setGraphGroupsMultiple(compareGraphPage.graphs_json[i], response['nodes'], i); + // compareGraphPage.setGraphGroupsMultiple(compareGraphPage.graphs_json[i], response['nodes'], i); if (i > 0) { compareGraphPage.graphs_json[0]['elements']['nodes'] = compareGraphPage.graphs_json[0]['elements']['nodes'] @@ -692,11 +701,12 @@ var compareGraphPage = { $('#cyGraphContainer').css('height', '99%'); $('#visualization-li a:last').tab('show'); $('#visualization-li a:first').tab('show'); + compareGraphPage.cyGraph.nodes().style("display", "element"); }, 100); }); - compareGraphPage.populateNodeData(response['nodes']); - compareGraphPage.populateEdgeData(response['edges']); + // compareGraphPage.populateNodeData(response['nodes']); + // compareGraphPage.populateEdgeData(response['edges']); $('#nodes-total-badge').text(response['nodes'].length); $('#edges-total-badge').text(response['edges'].length); @@ -720,7 +730,6 @@ var compareGraphPage = { $('#nodes-li').show(); $('#visualization-li').show(); $('#edges-li').show(); - $('#visualization-li a:last').tab('show'); $('#visualization-li a:first').tab('show'); }, compareGraphHelper: function () { @@ -751,6 +760,7 @@ var compareGraphPage = { compareGraphPage.setGraphGroups(compareGraphPage.graphs_json[0], response['nodes'], 0); compareGraphPage.setGraphGroups(compareGraphPage.graphs_json[1], response['nodes'], 1); + compareGraphPage.graphs_json[0]['elements']['nodes'] = compareGraphPage.graphs_json[0]['elements']['nodes'].concat(compareGraphPage.graphs_json[1]['elements']['nodes']); compareGraphPage.graphs_json[0]['elements']['edges'] = compareGraphPage.graphs_json[0]['elements']['edges'].concat(compareGraphPage.graphs_json[1]['elements']['edges']); compareGraphPage.styles_json[0]['style'] = compareGraphPage.styles_json[0]['style'].concat(compareGraphPage.styles_json[1]['style']); @@ -759,14 +769,13 @@ var compareGraphPage = { compareGraphPage.populateNodeData(response['nodes']); compareGraphPage.populateEdgeData(response['edges']); - // compareGraphPage.setCommonElements(response['nodes']); + compareGraphPage.cyGraph.ready(function () { // Wait for cytoscape to actually load and map eles - // compareGraphPage.formatCyGraph(); + compareGraphPage.cyGraph.panzoom(); window.setTimeout(function () { $('#cyGraphContainer').css('height', '99%'); - $('#visualization-li a:last').tab('show'); - $('#visualization-li a:first').tab('show'); + compareGraphPage.cyGraph.nodes().style("display", "element"); }, 100); }); @@ -774,8 +783,6 @@ var compareGraphPage = { window.setTimeout(function () { compareGraphPage.cyGraph.reset().fit().center(); $('#cyGraphContainer').css('height', '99%'); - $('#visualization-li a:last').tab('show'); - $('#visualization-li a:first').tab('show'); }, 100); }); @@ -959,6 +966,8 @@ var compareGraphPage = { */ layout = { name: 'cola', + animate: true, + maxSimulationTime: 1000, }; let y_max = 0; let y_min = Infinity; @@ -972,10 +981,10 @@ var compareGraphPage = { if ('position' in node) { newNode['position'] = node['position']; newNode['classes'] = node['classes'] - if (node['data']['parent'] == 'graph_1') { + if (node['classes'] == 'graph_1') { x_max = (x_max > node['position']['x']) ? x_max : node['position']['x']; } - if (node['data']['parent'] == 'common_1') { + if (node['classes'] == 'common_1') { c_max = (c_max > node['position']['x']) ? c_max : node['position']['x']; c_min = (c_min < node['position']['x']) ? c_min : node['position']['x']; } @@ -992,10 +1001,10 @@ var compareGraphPage = { _.map(graph_json['elements']['nodes'], function (node) { if ('position' in node) { - if (node['data']['parent'] == 'graph_2') { + if (node['classes'] == 'graph_2') { node['position']['x'] = node['position']['x'] + x_max + 100; } - if (node['data']['parent'] == 'common_1') { + if (node['classes'] == 'common_1') { node['position']['x'] = node['position']['x'] + Math.abs((c_max - c_min) / 2 - x_max) + 100; node['position']['y'] = node['position']['y'] + y_max + 50; } @@ -1030,6 +1039,7 @@ var compareGraphPage = { $('#dialog').dialog({ autoOpen: false }); + this.nodes().style("display", "none"); // display node data as a popup this.on('tap', graphPage.onTapGraphElement); From 31c24ffdbf132454c373cacfa9262a91098078ed Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 6 Aug 2019 12:13:18 +0200 Subject: [PATCH 087/105] Add compare checkbox in index-page --- static/js/graphs_page.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index 4c949f22..392f2021 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -226,7 +226,9 @@ var graphsPage = { }, graphNameFormatter: function (value, row) { var queryString = (graphsPage.searchBar.val() && graphsPage.searchBar.val().length > 0) ? '?query=' + _.join(graphsPage.searchBar.val(), ',') : ''; - return $('').attr('href', '/graphs/' + row.id + queryString).text(value)[0].outerHTML; + var checkbox = $('').attr('type', 'checkbox').attr('row_id', row.id).attr('onclick', 'compareGraphPage.selectGraphForCompare(' + row.id +')').text(value)[0].outerHTML; + return checkbox + $('').attr('href', '/graphs/' + row.id + queryString).text(value)[0].outerHTML; + // return $('').attr('href', '/graphs/' + row.id + queryString).text(value)[0].outerHTML; }, tagsFormatter: function (value, row) { links = []; From 9e7b97074c7dd68199d90a16622ed7590d98cf45 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 6 Aug 2019 12:13:57 +0200 Subject: [PATCH 088/105] Add graph_ids for multiple graphs --- templates/compare_graph/compare_graphs.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compare_graph/compare_graphs.html b/templates/compare_graph/compare_graphs.html index f65568b2..8cc57a33 100644 --- a/templates/compare_graph/compare_graphs.html +++ b/templates/compare_graph/compare_graphs.html @@ -5,6 +5,7 @@ {% if not Error %} var graph_1_id = {% if graph_1_id %}{{ graph_1_id|safe }}{% else %}null{% endif %}; var graph_2_id = {% if graph_2_id %}{{ graph_2_id|safe }}{% else %}null{% endif %}; + var graph_ids = {% if graph_ids %}{{ graph_ids|safe }}{% else %}null{% endif %}; var operation = {% if operation %}{{ operation|safe }}{% else %}null{% endif %}; {% endif %} From 1838631076903af57b50be9208b893bf0b6d2881 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 6 Aug 2019 12:14:28 +0200 Subject: [PATCH 089/105] Refactor reset button UI --- templates/compare_graph/compare_modal.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/compare_graph/compare_modal.html b/templates/compare_graph/compare_modal.html index 1d267a8c..17a54c5e 100644 --- a/templates/compare_graph/compare_modal.html +++ b/templates/compare_graph/compare_modal.html @@ -128,12 +128,13 @@ {#
  • Difference
  • #} +

    - - + +
    From 3c4e8c23f7196f442534c43138af243deca04989 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 6 Aug 2019 12:15:41 +0200 Subject: [PATCH 090/105] Add graph comparison dropdown in index page --- .../compare_graph/compare_visualization.html | 2 +- templates/graphs/index.html | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/templates/compare_graph/compare_visualization.html b/templates/compare_graph/compare_visualization.html index 02bed206..75068925 100644 --- a/templates/compare_graph/compare_visualization.html +++ b/templates/compare_graph/compare_visualization.html @@ -1,6 +1,6 @@
    -
    +
    diff --git a/templates/graphs/index.html b/templates/graphs/index.html index 7a37104b..bacd8e46 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -53,9 +53,27 @@ wnt,tags:kegg-networks paper_title:Xtalk
    + +
    @@ -91,6 +109,7 @@ + - + diff --git a/templates/graph/index.html b/templates/graph/index.html index 7aceed39..526bde6d 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -299,7 +299,7 @@

    - diff --git a/templates/graphs/index.html b/templates/graphs/index.html index bacd8e46..3de705b0 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -57,7 +57,7 @@ - diff --git a/templates/graphs/owned_graphs_table.html b/templates/graphs/owned_graphs_table.html index 4cdf4e16..02c79e24 100644 --- a/templates/graphs/owned_graphs_table.html +++ b/templates/graphs/owned_graphs_table.html @@ -1,5 +1,7 @@ Date: Tue, 20 Aug 2019 22:45:14 +0200 Subject: [PATCH 101/105] Local settings --- applications/graphs/models.py | 4 ++-- graphspace/database.py | 5 +++++ graphspace/settings/local.py | 6 +++--- requirements.txt | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/applications/graphs/models.py b/applications/graphs/models.py index f0e3e554..f68d5244 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -17,8 +17,8 @@ class Graph(IDMixin, TimeStampMixin, Base): name = Column(String, nullable=False) owner_email = Column(String, ForeignKey('user.email', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) - graph_json = Column(String, nullable=False) - style_json = Column(String, nullable=False) + graph_json = Column(String) + style_json = Column(String) is_public = Column(Integer, nullable=False, default=0) default_layout_id = Column(Integer, ForeignKey('layout.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=True) diff --git a/graphspace/database.py b/graphspace/database.py index b750a367..f92025bf 100644 --- a/graphspace/database.py +++ b/graphspace/database.py @@ -20,6 +20,11 @@ def __init__(self): self.engine = create_engine(''.join( ['postgresql://', config['USER'], ':', config['PASSWORD'], '@', config['HOST'], ':', config['PORT'], '/', config['NAME']]), echo=False) # TODO: Find out what is the use of metadata and reflection. + self.connection = self.engine.connect() + result = self.connection.execute("SELECT * FROM pg_extension where extname like 'pg_trgm'") + if result.rowcount==0: + self.connection.execute("create extension btree_gin") + self.connection.execute("create extension pg_trgm") settings.BASE.metadata.create_all(self.engine) self.meta = sqlalchemy.schema.MetaData() self.meta.reflect(bind=self.engine) diff --git a/graphspace/settings/local.py b/graphspace/settings/local.py index b15da20d..ac0a5f16 100644 --- a/graphspace/settings/local.py +++ b/graphspace/settings/local.py @@ -40,9 +40,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'test_database', - 'USER': 'adb', - 'PASSWORD': '', + 'NAME': 'graphspace', + 'USER': 'postgres', + 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '5432' } diff --git a/requirements.txt b/requirements.txt index 1f6c5be8..67c5bd1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ MarkupSafe==0.23 networkx==1.11 oauthlib==1.1.2 poster==0.8.1 -psycopg2==2.6.2 +psycopg2 py-bcrypt==0.4 Pygments==2.1.3 pytz==2016.4 From 18ed3e8c1de3ee32d5f0e80fef511078eca2d7c0 Mon Sep 17 00:00:00 2001 From: jahandaniyal Date: Tue, 20 Aug 2019 22:14:35 +0200 Subject: [PATCH 102/105] Bug fixes and refactoring --- applications/graphs/controllers.py | 4 +-- applications/graphs/dal.py | 34 +++++++++++++++++++++ static/js/graphs_compare.js | 30 ++++++++++++------- templates/base.html | 4 +-- templates/graph/index.html | 2 +- templates/graphs/index.html | 36 ++++++++++++++++++++++- templates/graphs/owned_graphs_table.html | 2 ++ templates/graphs/public_graphs_table.html | 2 ++ templates/graphs/shared_graphs_table.html | 2 ++ 9 files changed, 100 insertions(+), 16 deletions(-) diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index f9e09314..07a8a612 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -730,8 +730,8 @@ def get_graphs_difference_multi(request, graphs): nodes & edges: object """ - node_data = db.nodes_difference(request.db_session, graph_1, graph_2) - edge_data = db.edges_difference(request.db_session, graph_1, graph_2) + node_data = db.nodes_difference_multi(request.db_session, graphs) + edge_data = db.edges_difference_multi(request.db_session, graphs) return node_data, edge_data diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 12789313..c088608a 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -627,6 +627,23 @@ def nodes_intersection_multi(db_session, graphs): return total, query.all() +@with_session +def nodes_difference_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Node).filter(Node.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Node).filter(Node.graph_id == graph_id).subquery() + query = query.outerjoin(sub_q, sub_q.c.name == Node.name).\ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + @with_session def edges_intersection_multi(db_session, graphs): """ @@ -644,6 +661,23 @@ def edges_intersection_multi(db_session, graphs): return total, query.all() +@with_session +def edges_difference_multi(db_session, graphs): + """ + Find node intersection of N Graphs. + :param db_session: Database session. + :param graph: Unique IDs of the Graphs. + :return: list: total, queried nodes + """ + query = db_session.query(Edge).filter(Edge.graph_id == graphs[0]) + for graph_id in graphs[1:]: + sub_q = db_session.query(Edge).filter(Edge.graph_id == graph_id).subquery() + query = query.join(sub_q, sub_q.c.head_node_name == Edge.head_node_name)\ + .filter(Edge.tail_node_name == sub_q.c.tail_node_name).filter(sub_q.c.head_node_name == None) + total = query.count() + return total, query.all() + + @with_session def nodes_comparison(db_session, comp_expression=None): """ diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js index 6c36dcb8..2a51d8af 100644 --- a/static/js/graphs_compare.js +++ b/static/js/graphs_compare.js @@ -44,13 +44,15 @@ var compareGraphPage = { compareGraphPage.compareGraphs(); }); */ - $("#dropdownMenu1").on("focusin", function () { + $("#dropdownMenu1").on("focusout", function () { var value = ""; $("#search-place-holder").val(""); $(".dropdown-menu li").filter(function () { $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) }); - + if( $('#dropdownMenu1').val() && $('#dropdownMenu2').val() && $('#operatorMenu1').val()){ + compareGraphPage.compareGraphs(); + } }); $("#dropdownMenu2").on("focusin", function () { var value = ""; @@ -141,19 +143,27 @@ var compareGraphPage = { */ if ($('input:checkbox:checked').length > 1) { console.log('Compare'); - $('#comparehref > li > a')[0].setAttribute('href', href + 'operation=intersection'); - $('#comparehref > li > a')[1].setAttribute('href', href + 'operation=difference'); - $('#compareHrefDiv').css('visibility', 'visible'); + _.each($('[id^=comparehref] > li > a[data="Intersection"]'), function(item){ + item.setAttribute('href', href + 'operation=intersection'); + }); + _.each($('[id^=comparehref] > li > a[data="Difference"]'), function(item){ + item.setAttribute('href', href + 'operation=difference'); + }); + $('[id^=compareHrefDiv]').css('visibility', 'visible'); } else { - $('#compareHrefDiv').css('visibility', 'hidden'); + $('[id^=compareHrefDiv]').css('visibility', 'hidden'); } if ($('input:checkbox:checked').length == 2) { console.log('Compare'); - $('#comparehref > li > a')[0].setAttribute('href', '/compare?graph_1=' + compareGraphPage.graph_ids[0] - + '&graph_2=' + compareGraphPage.graph_ids[1] + '&operation=intersection'); - $('#comparehref > li > a')[1].setAttribute('href', '/compare?graph_1=' + compareGraphPage.graph_ids[0] + _.each($('[id^=comparehref] > li > a[data="Intersection"]'), function(item){ + item.setAttribute('href', '/compare?graph_1=' + compareGraphPage.graph_ids[0] + + '&graph_2=' + compareGraphPage.graph_ids[1] + '&operation=intersection'); + }); + _.each($('[id^=comparehref] > li > a[data="Difference"]'), function(item){ + item.setAttribute('href', '/compare?graph_1=' + compareGraphPage.graph_ids[0] + '&graph_2=' + compareGraphPage.graph_ids[1] + '&operation=difference'); - $('#compareHrefDiv').css('visibility', 'visible'); + }); + $('[id^=compareHrefDiv]').css('visibility', 'visible'); } }, colorPickerHelper: function (obj, event, graph_id, common) { diff --git a/templates/base.html b/templates/base.html index 601bbf03..1f7525d7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,7 +17,7 @@ - + @@ -47,7 +47,7 @@ - + diff --git a/templates/graph/index.html b/templates/graph/index.html index b138acfb..7f865bf1 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -343,7 +343,7 @@

    - diff --git a/templates/graphs/index.html b/templates/graphs/index.html index bacd8e46..3de705b0 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -57,7 +57,7 @@ - diff --git a/templates/graphs/owned_graphs_table.html b/templates/graphs/owned_graphs_table.html index 4cdf4e16..02c79e24 100644 --- a/templates/graphs/owned_graphs_table.html +++ b/templates/graphs/owned_graphs_table.html @@ -1,5 +1,7 @@

    Date: Wed, 21 Aug 2019 16:27:52 +0200 Subject: [PATCH 103/105] Fix custom-toolbar alignment --- static/js/graphs_compare.js | 1 + templates/graphs/index.html | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js index 2a51d8af..27e205ad 100644 --- a/static/js/graphs_compare.js +++ b/static/js/graphs_compare.js @@ -141,6 +141,7 @@ var compareGraphPage = { href += 'id=' + item.getAttribute('row_id') + '&'; }); */ + $('.bootstrap-table > .fixed-table-toolbar').addClass('pull-right'); if ($('input:checkbox:checked').length > 1) { console.log('Compare'); _.each($('[id^=comparehref] > li > a[data="Intersection"]'), function(item){ diff --git a/templates/graphs/index.html b/templates/graphs/index.html index 3de705b0..ca638fae 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -57,7 +57,7 @@ - '); $('#edges-table > thead > tr').append(''); - apis.compare.get({ - 'graph_1_id': compareGraphPage.graph_ids[0], - 'graph_2_id': compareGraphPage.graph_ids[1], - 'operation': operation - }, + apis.compare.get(data, successCallback = function (response) { // $('#nodes-table').DataTable(); compareGraphPage.common_nodes = response['nodes']; diff --git a/templates/compare_graph/compare_graphs.html b/templates/compare_graph/compare_graphs.html index ab4d11f1..9e09ea22 100644 --- a/templates/compare_graph/compare_graphs.html +++ b/templates/compare_graph/compare_graphs.html @@ -6,6 +6,7 @@ var graph_1_id = {% if graph_1_id %}{{ graph_1_id|safe }}{% else %}null{% endif %}; var graph_2_id = {% if graph_2_id %}{{ graph_2_id|safe }}{% else %}null{% endif %}; var graph_ids = {% if graph_ids %}{{ graph_ids|safe }}{% else %}null{% endif %}; + var version = {% if version %}{{ version|safe }}{% else %}null{% endif %}; var operation = {% if operation %}{{ operation|safe }}{% else %}null{% endif %}; {% endif %} diff --git a/templates/graphs/index.html b/templates/graphs/index.html index 7d1b76f8..0700cff7 100644 --- a/templates/graphs/index.html +++ b/templates/graphs/index.html @@ -68,40 +68,6 @@ Compare Selected Graphs - -

    Graph 1

    Graph 2