diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..9de17e17 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Purpose +_Describe the problem or feature in addition to a link to the issues._ + +Example: +Fixes # . + +## Approach +_How does this change address the problem?_ + +#### Open Questions and Pre-Merge TODOs +- [ ] Use github checklists. When solved, check the box and explain the answer. + +## Learning +_Describe the research stage_ + +_Links to blog posts, patterns, libraries or addons used to solve this problem_ + +#### Blog Posts +- [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template. + diff --git a/api_specifications/api.raml b/api_specifications/api.raml index 1ca96ef4..512b840c 100644 --- a/api_specifications/api.raml +++ b/api_specifications/api.raml @@ -543,6 +543,32 @@ types: graph_id: number positions_json: string style_json: string + Fork: + description: Fork Graph object + example: | + { + "graph_id": 21, + "parent_graph_id": 25 + } + properties: + graph_id: number + parent_graph_id: number + ForkResponse: + description: Fork Graph Response object + example: | + { + "graph_id": 21, + "parent_graph_id": 25, + "updated_at": "2017-03-27T07:59:35.416245", + "created_at": "2017-03-27T07:59:35.416245", + "id": 1235 + } + properties: + id: number + graph_id: number + parent_graph_id: number + created_at: string + updated_at: string Member: description: Member Response object example: | @@ -808,6 +834,90 @@ types: body: application/json: type: Error + /fork: + description: APIs to access graph forks on GraphSpace. + displayName: Graph Forks + get: + description: List all Graph Forks matching query criteria, if provided; otherwise list all Graph Forks. + queryParameters: + parent_graph_id?: + description: Search for Graph Forks with given parent_graph_id. + type: number + responses: + 200: + description: SUCCESS + body: + application/json: + type: object + properties: + total: number + layouts: ForkResponse[] + 400: + description: BAD REQUEST + body: + application/json: + type: Error + 403: + description: FORBIDDEN + body: + application/json: + type: Error + post: + description: Create a new Fork of a Graph. + body: + application/json: + type: Fork + responses: + 201: + description: SUCCESS + body: + application/json: + type: ForkResponse + 400: + description: BAD REQUEST + body: + application/json: + type: Error + 403: + description: FORBIDDEN + body: + application/json: + type: Error + /{graph_id}: + description: APIs to access a specific Fork on GraphSpace. + displayName: Graph Fork + get: + description: Get a Fork by forked graph id + responses: + 200: + description: SUCCESS + body: + application/json: + type: ForkResponse + 403: + description: FORBIDDEN + body: + application/json: + type: Error + delete: + description: Delete a Fork by forked graph id + responses: + 200: + body: + application/json: + type: object + properties: + message: string + example: | + { + "message": "Successfully deleted Fork with id=1068" + } + 403: + description: FORBIDDEN + body: + application/json: + type: Error + /groups: description: APIs to access groups on GraphSpace. diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 7f012a05..74dc3c92 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -39,6 +39,23 @@ def map_attributes(attributes): def get_graph_by_id(request, graph_id): return db.get_graph_by_id(request.db_session, graph_id) +def get_fork_by_id(request, graph_id): + return db.get_fork_by_id(request.db_session, graph_id) + +def find_forks(request, parent_graph_id=None, forked_graph_id=None): + if parent_graph_id is None: + raise Exception("The Parent graph ID is required.") + return db.get_fork(request.db_session, parent_graph_id, forked_graph_id) + +def get_forked_graph_data(request, forked_graph, graph_fork): + '''Set information of parent graph into Graph object. The modified graph object is send in the HTTP Response''' + if graph_fork: + parent_graph = get_graph_by_id(request, graph_fork.parent_graph_id) + forked_graph['parent_email'] = parent_graph.owner_email + forked_graph['parent_graph_name'] = parent_graph.name + forked_graph['is_fork'] = 1 + forked_graph['parent_id'] = parent_graph.id + return forked_graph def is_user_authorized_to_view_graph(request, username, graph_id): is_authorized = False @@ -333,10 +350,11 @@ def delete_graph_to_group(request, group_id, graph_id): def search_graphs1(request, owner_email=None, names=None, nodes=None, edges=None, tags=None, member_email=None, - is_public=None, query=None, limit=20, offset=0, order='desc', sort='name'): + is_public=None, is_forked=None, query=None, limit=20, offset=0, order='desc', sort='name'): sort_attr = getattr(db.Graph, sort if sort is not None else 'name') orber_by = getattr(db, order if order is not None else 'desc')(sort_attr) is_public = int(is_public) if is_public is not None else None + is_forked = int(is_forked) if is_forked is not None else None if member_email is not None: member_user = users.controllers.get_user(request, member_email) @@ -362,6 +380,7 @@ def search_graphs1(request, owner_email=None, names=None, nodes=None, edges=None owner_email=owner_email, graph_ids=graph_ids, is_public=is_public, + is_forked=is_forked, group_ids=group_ids, names=names, nodes=nodes, @@ -586,3 +605,11 @@ 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 add_graph_to_fork(request, forked_graph_id, parent_graph_id, owner_email): + if forked_graph_id is not None and parent_graph_id is not None: + new_fork = db.add_fork(request.db_session, forked_graph_id, parent_graph_id, owner_email) + else: + raise Exception("Required Parameter is missing!") + return new_fork diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 388b8141..65bd7189 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -124,9 +124,11 @@ def delete_graph(db_session, id): def get_graph_by_id(db_session, id): return db_session.query(Graph).filter(Graph.id == id).one_or_none() +def get_fork_by_id(db_session, id): + return db_session.query(GraphFork).filter(GraphFork.graph_id == id).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, +def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is_public=None, is_forked=None, names=None, nodes=None, edges=None, tags=None, limit=None, offset=None, order_by=desc(Graph.updated_at)): query = db_session.query(Graph) @@ -151,6 +153,8 @@ def find_graphs(db_session, owner_email=None, group_ids=None, graph_ids=None, is options_group.append(subqueryload('nodes')) if edges is not None and len(edges) > 0: options_group.append(subqueryload(Graph.edges)) + if is_forked is not None: + options_group.append(joinedload(Graph.forked_graphs)) if group_ids is not None and len(group_ids) > 0: options_group.append(joinedload('shared_with_groups')) if len(options_group) > 0: @@ -159,6 +163,9 @@ 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))) + if is_forked is not None: + query = query.filter(Graph.forked_graphs.any()) + edges = [] if edges is None else edges nodes = [] if nodes is None else nodes names = [] if names is None else names @@ -458,3 +465,21 @@ 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 add_fork(db_session, forked_graph_id, parent_graph_id, owner_email): + fork = GraphFork( graph_id=forked_graph_id, parent_graph_id=parent_graph_id) + db_session.add(fork) + return fork + + +@with_session +def get_fork(db_session, parent_graph_id=None, forked_graph_id=None): + query = db_session.query(GraphFork) + if parent_graph_id is not None: + query = query.filter(GraphFork.parent_graph_id == parent_graph_id) + if forked_graph_id is not None: + query = query.filter(GraphFork.graph_id == forked_graph_id) + total = query.count() + return total, query.all() diff --git a/applications/graphs/models.py b/applications/graphs/models.py index f0e3e554..1af5468b 100644 --- a/applications/graphs/models.py +++ b/applications/graphs/models.py @@ -32,6 +32,8 @@ 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") + forked_graphs = relationship("GraphFork", foreign_keys="GraphFork.graph_id", back_populates="graph", cascade="all, delete-orphan") + groups = association_proxy('shared_with_groups', 'group') tags = association_proxy('graph_tags', 'tag') @@ -279,3 +281,29 @@ class GraphToTag(TimeStampMixin, Base): def __table_args__(cls): args = cls.constraints + cls.indices return args + + +class GraphFork(IDMixin, TimeStampMixin, Base): + __tablename__ = 'graph_fork' + #name = Column(String, nullable=False) + #owner_email = Column(String, ForeignKey('user.email', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + parent_graph_id = Column(Integer, ForeignKey('graph.id', ondelete="CASCADE", onupdate="CASCADE"), nullable=False) + + graph = relationship("Graph", foreign_keys=[graph_id], back_populates="forked_graphs", uselist=False) + + indices = (Index('graph2fork_idx_graph_id_parent_id', 'graph_id', 'parent_graph_id'),) + constraints = () + + @declared_attr + def __table_args__(cls): + args = cls.constraints + cls.indices + return args + + def serialize(cls, **kwargs): + return { + 'graph_id': cls.graph_id, + 'parent_graph_id': cls.parent_graph_id, + 'created_at': cls.created_at.isoformat(), + 'updated_at': cls.updated_at.isoformat() + } diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 297f92ab..440bd0b2 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -27,6 +27,8 @@ # 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 Fork + url(r'^ajax/graphs/(?P[^/]+)/fork/$', views.graph_fork_ajax_api, name='graph_fork_ajax_api'), # REST APIs Endpoints @@ -45,5 +47,7 @@ # 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 Fork + url(r'^api/v1/graphs/(?P[^/]+)/fork/$', views.graph_fork_rest_api, name='graph_fork_rest_api'), ] diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 772639c5..6b2a851d 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -5,7 +5,7 @@ import graphspace.authorization as authorization import graphspace.utils as utils from django.conf import settings -from django.http import HttpResponse, QueryDict +from django.http import HttpResponse, QueryDict, HttpResponseForbidden from django.shortcuts import render, redirect from django.template import RequestContext from django.views.decorators.csrf import csrf_exempt @@ -212,9 +212,10 @@ def graphs_advanced_search_ajax_api(request): if user_role == authorization.UserRole.LOGGED_IN: if queryparams.get('owner_email', None) is None \ and queryparams.get('member_email', None) is None \ - and queryparams.get('is_public', None) != '1': + and queryparams.get('is_public', None) != '1' \ + and queryparams.get('is_forked', None) != '1' : raise BadRequest(request, error_code=ErrorCodes.Validation.IsPublicNotSet) - if queryparams.get('is_public', None) != '1': + if queryparams.get('is_public', None) != '1' and queryparams.get('is_forked', None) != '1': if get_request_user(request) != queryparams.get('member_email', None) \ and get_request_user(request) != queryparams.get('owner_email', None): raise BadRequest(request, error_code=ErrorCodes.Validation.NotAllowedGraphAccess, @@ -225,6 +226,7 @@ def graphs_advanced_search_ajax_api(request): member_email=queryparams.get('member_email', None), names=list(filter(None, queryparams.getlist('names[]', []))), is_public=queryparams.get('is_public', None), + is_forked=queryparams.get('is_forked', None), nodes=list(filter(None, queryparams.getlist('nodes[]', []))), edges=list(filter(None, queryparams.getlist('edges[]', []))), tags=list(filter(None, queryparams.getlist('tags[]', []))), @@ -401,8 +403,11 @@ def _get_graph(request, graph_id): """ authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) - - return utils.serializer(graphs.get_graph_by_id(request, graph_id)) + graph_by_id = utils.serializer(graphs.get_graph_by_id(request, graph_id)) + graph_fork = graphs.get_fork_by_id(request, graph_id) + if graph_fork: + graph_by_id = graphs.get_forked_graph_data(request, graph_by_id, graph_fork) + return graph_by_id def _add_graph(request, graph={}): @@ -1522,3 +1527,215 @@ 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 Fork APIs +''' + +@csrf_exempt +@is_authenticated() +def graph_fork_rest_api(request, graph_id=None): + """ + Handles any request sent to following urls: + /api/v1/graphs + /api/v1/graphs/ + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + """ + return _fork_api(request, graph_id=graph_id) + + +def graph_fork_ajax_api(request, graph_id=None): + """ + Handles any request sent to following urls: + /ajax/graphs + /ajax/graphs/ + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + """ + return _fork_api(request, graph_id=graph_id) + +def _fork_api(request, graph_id=None): + """ + Handles any request sent to following urls: + /graphs//fork + + Parameters + ---------- + request - HTTP Request + + Returns + ------- + response : JSON Response + + Raises + ------ + MethodNotAllowed: If a user tries to send requests other than GET or POST. + BadRequest: If HTTP_ACCEPT header is not set to application/json. + + """ + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if request.method == "GET" and graph_id is not None: + return HttpResponse(json.dumps(_get_forks(request, query=request.GET)), content_type="application/json") + elif request.method == "GET" and graph_id is not None: + return HttpResponse(json.dumps(_get_fork(request, graph_id)), content_type="application/json", + status=200) + elif request.method == "POST" and graph_id is not None: + return HttpResponse(json.dumps(_add_fork(request, graph=json.loads(request.body))), + content_type="application/json", status=201) + return HttpResponse(json.dumps({ + "message": "Successfully deleted graph with id=%s" % graph_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 _add_fork(request, graph={}): + """ + Graph Parameters + ---------- + name : string + Name of group. Required + owner_email : string + Email of the Owner of the graph. Required + tags: list of strings + List of tags to be attached with the graph. Optional + + + Parameters + ---------- + graph : dict + Dictionary containing the data of the graph being added. + request : object + HTTP POST Request. + + Returns + ------- + Confirmation Message + + Raises + ------ + BadRequest - Cannot create graph for user other than the requesting user. + + Notes + ------ + + """ + + # Validate add graph API request + user_role = authorization.user_role(request) + if user_role == authorization.UserRole.LOGGED_IN: + if get_request_user(request) != graph.get('owner_email', None): + raise BadRequest(request, error_code=ErrorCodes.Validation.CannotCreateGraphForOtherUser, + args=graph.get('owner_email', None)) + elif user_role == authorization.UserRole.LOGGED_OFF and graph.get('owner_email', None) is not None: + raise BadRequest(request, error_code=ErrorCodes.Validation.CannotCreateGraphForOtherUser, + args=graph.get('owner_email', None)) + try: + new_graph = graphs.add_graph(request, + name=graph.get('name', None), + is_public=graph.get('is_public', None), + graph_json=graph.get('graph_json', None), + style_json=graph.get('style_json', None), + tags=graph.get('tags', None), + owner_email=graph.get('owner_email', None)) + except Exception as error: + return error + return utils.serializer(graphs.add_graph_to_fork(request, + forked_graph_id=new_graph.id, + parent_graph_id=graph.get('parent_id', None), + owner_email=graph.get('owner_email', None))) + +def _get_forks(request, query=dict()): + """ + Query Parameters + ---------- + parent_graph_id : integer + ID of the Parent graph from which Graph was forked. + + Parameters + ---------- + query : dict + Dictionary of query parameters. + request : object + HTTP GET Request. + parent_graph_id : Integer + ID of the Parent graph from which Graph was forked. + + Returns + ------- + total : integer + Number of groups matching the request. + forks : List of Forked Graphs. + List of Forked Graphs for the given parent graph ID. + + 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 + ------ + """ + + querydict = QueryDict('', mutable=True) + querydict.update(query) + query = querydict + + # Validate search graphs API request + user_role = authorization.user_role(request) + if user_role == authorization.UserRole.LOGGED_IN: + if query.get('parent_graph_id', None) is None: + raise BadRequest(request, error_code=ErrorCodes.Validation.GraphIDMissing) + + total, forks_list = graphs.find_forks(request, + parent_graph_id=query.get('parent_graph_id', None), + forked_graph_id=query.get('forked_graph_id', None) + ) + + return { + 'total': total, + 'graphs': [utils.serializer(fork, summary=True) for fork in forks_list] + } + +def _get_fork(request, graph_id): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + graph_id : string + Unique ID of the graph. + + Returns + ------- + graph: object + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_id) + + return utils.serializer(graphs.get_fork_by_id(request, graph_id)) diff --git a/migration/versions/bdce7c016932_add_graph_fork_table.py b/migration/versions/bdce7c016932_add_graph_fork_table.py new file mode 100644 index 00000000..9d249146 --- /dev/null +++ b/migration/versions/bdce7c016932_add_graph_fork_table.py @@ -0,0 +1,34 @@ +"""add_graph_fork_table + +Revision ID: bdce7c016932 +Revises: bb9a45e2ee5e +Create Date: 2018-05-19 16:15:09.911000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'bdce7c016932' +down_revision = 'bb9a45e2ee5e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'graph_fork', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('graph_id', sa.Integer, nullable=False), + sa.Column('parent_graph_id', sa.Integer, nullable=False), + ) + op.add_column('graph_fork', sa.Column('created_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + op.add_column('graph_fork', sa.Column('updated_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())) + # Create New Index + op.create_index('graph2fork_idx_graph_id_parent_id', 'graph_fork', ['graph_id', 'parent_graph_id'], unique=True) + # Add new foreign key reference + op.execute('ALTER TABLE graph_fork ADD CONSTRAINT fork_graph_id_fkey FOREIGN KEY (graph_id) REFERENCES "graph" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;') + + +def downgrade(): + op.drop_table('graph_fork') diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index a7f4c57a..c2d52836 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -39,6 +39,18 @@ var apis = { apis.jsonRequest('GET', apis.nodes.ENDPOINT({'graph_id': graph_id}), data, successCallback, errorCallback) }, }, + fork: { + ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/fork/'), + add: function(graph_id, data, successCallback, errorCallback) { + apis.jsonRequest('POST', apis.fork.ENDPOINT({'graph_id': graph_id}), data, successCallback, errorCallback) + }, + getByID: function (graph_id, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.fork.ENDPOINT({'graph_id': graph_id}), undefined, successCallback, errorCallback) + }, + get: function (graph_id, data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.fork.ENDPOINT({'graph_id': graph_id}), data, successCallback, errorCallback) + } + }, edges: { ENDPOINT: _.template('/ajax/graphs/<%= graph_id %>/edges/'), get: function (graph_id, data, successCallback, errorCallback) { @@ -382,6 +394,40 @@ var graphsPage = { ); } }, + forkedGraphsTable: { + getForkedGraphs: function(params) { + /** + * This is the custom ajax request used to load graphs in forkedGraphsTable. + * + * params - query parameters for the ajax request. + * It contains parameters like limit, offset, search, sort, order. + */ + $('#ForkedGraphsTable').bootstrapTable('showLoading'); + $('#forkedGraphsTotal').html(''); + + query = params.data['query'] + delete params.data.query; + + params.data["is_forked"] = 1; + params.data["owner_email"] = $('#UserEmail').val(); + + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + $('#ForkedGraphsTable').bootstrapTable('hideLoading'); + params.success(response); + $('#forkedGraphsTotal').text(response.total); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + $('#ForkedGraphsTable').bootstrapTable('hideLoading'); + params.error('Error'); + } + ); + }, + }, + + }; //var graph ends var uploadGraphPage = { @@ -441,6 +487,13 @@ var graphPage = { utils.initializeTabs(); + $("#ForkGraph").click(function (e) { + e.preventDefault(); + if (!$(this).hasClass('disabled')){ + $('#forkGraphModal').data('data', $(this).data()).modal('show');//graphPage.fork($(this).data()); // Passing data about parent graph to fork() as argument + } + }); + $('#saveOnExitLayoutBtn').click(function () { graphPage.cyGraph.contextMenus('get').destroy(); // Destroys the cytocscape context menu extension instance. @@ -482,7 +535,7 @@ var graphPage = { $('#ConfirmRemoveLayoutBtn').click(graphPage.layoutsTable.onConfirmRemoveGraph); $('#ConfirmUpdateLayoutBtn').click(graphPage.layoutsTable.onConfirmUpdateGraph); - + $('#ConfirmForkGraphBtn').click(graphPage.fork); this.filterNodesEdges.init(); this.colaLayoutWidget.init(); @@ -516,6 +569,33 @@ var graphPage = { graphPage.defaultLayoutWidget.init(); }, + fork: function () { + //Read the meta_data object and add 'parent_id' & 'parent_email' to the data field. + data = $('#forkGraphModal').data('data') + graph_name = $('#forkGraphName').val() + if (graph_name!="") data.graph_name = graph_name; + $('#forkGraphModal').modal('hide'); + //$.notify({message: 'Request to fork the graph has been submitted'}, {type: 'info'}); + graph_meta_data = cytoscapeGraph.getNetworkAndViewJSON(graphPage.cyGraph); + var graphData = { + 'name':data.graph_name, + 'is_public':0, + 'parent_id':data.graph_id, + 'owner_email':data.uid, + 'graph_json':graph_meta_data,//JSON.stringify(graph_meta_data, null, 4), + 'style_json':cytoscapeGraph.getStyleJSON(graphPage.cyGraph)//JSON.stringify(cytoscapeGraph.getStyleJSON(graphPage.cyGraph), null, 4) + } + + apis.fork.add(data.graph_id, graphData, + successCallback = function (response) { + $.notify({message: 'The Graph has been forked successfully'}, {type: 'success'}); + $("#ForkGraph").addClass('disabled'); + }, + errorCallback = function (response) { + $.notify({message: 'Could not fork the Graph due to the following error : ' + response.responseJSON.error_message.split("'")[1] + ''}, {type: 'danger'}); + } + ); + }, export: function (format) { cytoscapeGraph.export(graphPage.cyGraph, format, $('#GraphName').val()); }, diff --git a/templates/graph/index.html b/templates/graph/index.html index 830d44f3..e0dfe11a 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -139,12 +139,12 @@ - + {% include 'graphs/fork_graph_modal.html' %}
-
+

{{ title }}

@@ -169,9 +169,31 @@

{% endfor %}

{% endif %} + {% if 'data' in graph.graph_json and 'parent_email' in graph.graph_json.data%} +

+ forked from {{ graph.graph_json.data.parent_email }}/ {{ graph.name }} +

+ {% endif %} + {% if graph.is_fork %} +

+ Forked from {{ graph.parent_email }}/ {{ graph.parent_graph_name }} +

+ {% endif %}
-