diff --git a/README.md b/README.md index f0636274..ef8f9095 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,28 @@ WSGIScriptAlias / /path_to_GraphSpace/graphspace/wsgi.py Refer to https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/modwsgi/ if any problems occur with the setup. +Documentation +================= + +GraphSpace has extensive documentation on the [user interface](http://docs.graphspace.org/en/latest/Quick_Tour_of_GraphSpace.html#welcome-screen), the [REST API](http://docs.graphspace.org/en/latest/Programmers_Guide.html#graphspace-rest-api) and a [Python package for programmatic interaction](http://manual.graphspace.org/projects/graphspace-python/en/latest/tutorial/index.html). + + Contributing ================= Feel free to fork and send us pull requests. Here are the [guidelines for contribution](https://github.com/Murali-group/GraphSpace/blob/master/CONTRIBUTING.md) in GraphSpace. + + +Contact +================= + +If you have questions or suggestions about GraphSpace, please contact + +- **T.M. Murali ([@tmmurali](https://github.com/tmmurali))** +- **Aditya Bharadwaj ([@adbharadwaj](https://github.com/adbharadwaj))** + + +License +================= + +GraphSpace is available under the GNU General Public License v2.0 license. See [LICENSE.md](https://github.com/Murali-group/GraphSpace/blob/master/LICENSE.md) for more information. diff --git a/applications/graphs/controllers.py b/applications/graphs/controllers.py index 7f012a05..c8dab3ad 100644 --- a/applications/graphs/controllers.py +++ b/applications/graphs/controllers.py @@ -586,3 +586,25 @@ def add_edge(request, name=None, head_node_id=None, tail_node_id=None, is_direct def delete_edge_by_id(request, edge_id): db.delete_edge(request.db_session, id=edge_id) return + + +def get_graph_comparison(request, graph_1, graph_2, operation): + if operation == 'intersection': + return get_graphs_intersection(request, graph_1, graph_2) + else: + return get_graphs_difference(request, graph_1, graph_2) + + +def get_graphs_intersection(request, graph_1, graph_2): + # calling nodes_comparison function for testing purpose only + # db.nodes_comparison(request.db_session,) + + 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): + 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 diff --git a/applications/graphs/dal.py b/applications/graphs/dal.py index 388b8141..46164eb3 100644 --- a/applications/graphs/dal.py +++ b/applications/graphs/dal.py @@ -3,7 +3,7 @@ from applications.users.models import * from graphspace.wrappers import with_session -from sqlalchemy.orm import defer, undefer +from sqlalchemy.orm import defer, undefer, aliased @with_session @@ -458,3 +458,121 @@ def find_edges(db_session, is_directed=None, names=None, edges=None, graph_id=No query = query.limit(limit).offset(offset) return total, query.all() + + +@with_session +def nodes_intersection(db_session, graph_1_id=None, graph_2_id=None): + alias_node = aliased(Node) + query = db_session.query(Node, alias_node) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Node.graph_id == graph_1_id).\ + join(alias_node, Node.name == alias_node.name).\ + filter(alias_node.graph_id == graph_2_id) + total = query.count() + return total, query.all() + + +@with_session +def nodes_difference(db_session, graph_1_id=None, graph_2_id=None): + graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) + graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Node.name).\ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + +@with_session +def edges_intersection(db_session, graph_1_id=None, graph_2_id=None): + alias_edge = aliased(Edge) + query = db_session.query(Edge, alias_edge) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Edge.graph_id == graph_1_id).\ + join(alias_edge, Edge.head_node_name == alias_edge.head_node_name).\ + filter(alias_edge.graph_id == graph_2_id).\ + filter(Edge.tail_node_name == alias_edge.tail_node_name) + total = query.count() + return total, query.all() + + +@with_session +def edges_difference(db_session, graph_1_id=None, graph_2_id=None): + graph1_query = db_session.query(Edge).filter(Edge.graph_id == graph_1_id) + graph2_query = db_session.query(Edge).filter(Edge.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Edge.name). \ + filter(sub_q.c.name == None) + total = query.count() + return total, query.all() + + +@with_session +def nodes_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): + alias_node = aliased(Node) + if operation == 'i': + query = db_session.query(Node, alias_node) + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Node.graph_id == graph_1_id). \ + join(alias_node, Node.name == alias_node.name). \ + filter(alias_node.graph_id == graph_2_id) + return query.subquery() + else: + graph1_query = db_session.query(Node).filter(Node.graph_id == graph_1_id) + graph2_query = db_session.query(Node).filter(Node.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Node.name). \ + filter(sub_q.c.name == None) + return query.subquery() + +@with_session +def edges_subquery(db_session, graph_1_id=None, graph_2_id=None, operation=None): + alias_edge = aliased(Edge) + if operation == 'intersection': + query = db_session.query(Edge, alias_edge) + + if graph_1_id is not None and graph_2_id is not None: + query = query.filter(Edge.graph_id == graph_1_id). \ + join(alias_edge, Edge.head_node_name == alias_edge.head_node_name). \ + filter(alias_edge.graph_id == graph_2_id). \ + filter(Edge.tail_node_name == alias_edge.tail_node_name) + return query.subquery + else: + graph1_query = db_session.query(Edge).filter(Edge.graph_id == graph_1_id) + graph2_query = db_session.query(Edge).filter(Edge.graph_id == graph_2_id) + sub_q = graph2_query.subquery() + + query = graph1_query.outerjoin(sub_q, sub_q.c.name == Edge.name). \ + filter(sub_q.c.name == None) + return query.subquery + + +@with_session +def nodes_comparison(db_session, comp_expression=None): + # Infix -> a 'i' ( b - c ) + # Postfix -> a b c - 'i' + + comp_expression = [1, 2, 5, 'd', 'i'] + temp_stack = [] + for item in comp_expression: + if item not in ['d','i']: + temp_stack.append(item) + elif item in ['d','i']: + query1 = temp_stack.pop() + query2 = temp_stack.pop() + if type(query1) != int: + query = db_session.query(Node).filter(Node.graph_id == query2) + sub_query = query.outerjoin(query1, query1.c.name == Node.name). \ + filter(query1.c.name == None) + else: + sub_query = nodes_subquery(db_session, query1, query2, item) + temp_stack.append(sub_query) + query = temp_stack.pop() + count = query.count() + + return count, query.all() diff --git a/applications/graphs/urls.py b/applications/graphs/urls.py index 297f92ab..3c5ab587 100644 --- a/applications/graphs/urls.py +++ b/applications/graphs/urls.py @@ -8,6 +8,7 @@ url(r'^graphs/(?P[^/]+)$', views.graph_page, name='graph'), url(r'^graphs/(?P[^/]+)/(?P[^/]+)$', views.graph_page_by_name, name='graph_by_name'), url(r'^upload$', views.upload_graph_page, name='upload_graph'), + url(r'^compare$', views.compare_graph_page, name='compare_graph_page'), # AJAX APIs Endpoints @@ -15,6 +16,7 @@ url(r'^ajax/graphs/$', views.graphs_ajax_api, name='graphs_ajax_api'), url(r'^ajax/graphs/advanced_search$', views.graphs_advanced_search_ajax_api, name='graphs_advanced_search_ajax_api'), url(r'^ajax/graphs/(?P[^/]+)$', views.graphs_ajax_api, name='graph_ajax_api'), + url(r'^ajax/compare/$', views.compare_graphs, name='compare_graph'), # 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'), diff --git a/applications/graphs/views.py b/applications/graphs/views.py index 772639c5..ff69c0c2 100644 --- a/applications/graphs/views.py +++ b/applications/graphs/views.py @@ -14,6 +14,74 @@ from graphspace.wrappers import is_authenticated +def compare_graph_page(request): + 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: + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + + if request.GET.get('graph_1') and request.GET.get('graph_2') \ + and request.GET.get('operation'): + if request.GET.get('operation') == 'intersection' or request.GET.get('operation') == 'difference': + context['graph_1_id'] = json.dumps(request.GET.get('graph_1')) + context['graph_2_id'] = json.dumps(request.GET.get('graph_2')) + context['operation'] = json.dumps(request.GET.get('operation')) + return render(request, 'graphs/../../templates/compare_graph/compare_graphs.html', context) + else: + raise MethodNotAllowed(request) + + +def compare_graphs(request): + context = RequestContext(request, {}) + + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if request.method == "GET": + return HttpResponse(json.dumps(_compare_graph(request, request.GET['graph_1_id'], + request.GET['graph_2_id'], request.GET['operation'])), + content_type="application/json", status=200) + else: + raise MethodNotAllowed(request) # Handle other type of request methods like OPTIONS etc. + else: + raise MethodNotAllowed(request) + + +def _compare_graph(request, graph_1_id, graph_2_id, operation): + """ + + Parameters + ---------- + request : object + HTTP GET Request. + graph_1_id : string + Unique ID of the 1st graph. Required. + graph_2_id : string + Unique ID of the 2nd graph. Required. + operation : string + Comparison operation difference or intersection. Required. + + Returns + ------- + nodes & edges: object + + Raises + ------ + + Notes + ------ + + """ + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_1_id) + authorization.validate(request, permission='GRAPH_READ', graph_id=graph_2_id) + nodes, edges = graphs.get_graph_comparison(request, graph_1_id, graph_2_id, operation) + if operation == 'intersection': + edges = [[utils.serializer(edge) for edge in item] for item in edges[1]] + nodes = [[utils.serializer(node) for node in item] for item in nodes[1]] + else: + edges = [utils.serializer(edge) for edge in edges[1]] + nodes = [utils.serializer(node) for node in nodes[1]] + return {'edges': edges, 'nodes': nodes} + + def upload_graph_page(request): context = RequestContext(request, {}) diff --git a/applications/home/views.py b/applications/home/views.py index d9ddf1a0..a70eeb92 100644 --- a/applications/home/views.py +++ b/applications/home/views.py @@ -160,9 +160,9 @@ def forgot_password_page(request): if password_reset_code is not None: users.send_password_reset_email(request, password_reset_code) - context["success_message"] = "Email has been sent!" + context["success_message"] = "You will receive an email with link to update the password!" else: - context["error_message"] = "Email does not exist!" + context["error_message"] = "You will receive an email with link to update the password!" return render(request, 'forgot_password/index.html', context) # Handle POST request to forgot password page. else: raise MethodNotAllowed(request) # Handle other type of request methods like PUT, UPDATE. diff --git a/bower.json b/bower.json index 7b986c01..37f1dbbc 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,7 @@ "moment": "^2.17.0", "remarkable-bootstrap-notify": "^3.1.3", "animate.css": "^3.5.2", - "cytoscape": "^2.7.11", + "cytoscape": "^3.2.17", "webcola": "^3.3.0", "bootstrap": "^3.3.7", "cytoscape-cola": "^1.6.0", @@ -30,7 +30,7 @@ "bootstrap-table": "^1.11.0", "cytoscape-panzoom": "^2.4.0", "select2": "select2-dist#^4.0.3", - "cytoscape-context-menus": "^2.1.1", + "cytoscape-context-menus": "^3.0.6", "bootstrap-colorpicker": "^2.5.1" } } diff --git a/docs/Programmers_Guide.md b/docs/Programmers_Guide.md index f2434b9d..ee610fbc 100644 --- a/docs/Programmers_Guide.md +++ b/docs/Programmers_Guide.md @@ -50,3 +50,57 @@ Install graphspace_python from PyPI using: ### Usage Please refer to ``graphspace_python`` package's [documentation](http://manual.graphspace.org/projects/graphspace-python/) to learn how to use it. + + +## GraphSpace REST APIs using the Postman app + +### This documentation is based on [Sandeep Mahapatra's blog post](https://summerofcode17.wordpress.com/2017/05/30/using-the-graphspace-restful-api/) in the 2017 GSoc. + + +``` + Note: In order to fully utilize the features of GraphSpace REST API, you must have an account on GraphSpace. +``` + +Postman is a Google Chrome app for interacting with HTTP APIs. It provides a friendly GUI for constructing requests and reading responses. Postman makes it easy to test, develop and document APIs by allowing users to quickly put together both simple and complex HTTP requests. + +### Postman Installation + +Postman is available as a [native app](https://www.getpostman.com/docs/install_native) (recommended) for Mac / Windows / Linux, and as a Chrome App. The Postman Chrome app can only run on the Chrome browser. To use the Postman Chrome app, you need to: +- Install Google Chrome: [Install Chrome](https://www.google.com/chrome/). +- If you already have Chrome installed, head over to Postman’s page on the [Chrome Webstore](https://chrome.google.com/webstore/detail/postman-rest-client-packa/fhbjgbiflinjbdggehcddcbncdddomop?hl=en), and click ‘Add to Chrome’. +- After the download is complete, launch the app. + +### Using Postman for GraphSpace REST API + +The GraphSpace REST APIs have the base URL http://www.graphspace.org/api/v1/. There are many endpoints defined under this base URL (the documentation of which can be found here), but to learn and understand the usage of GraphSpace REST APIs through Postman, we would be considering only the /graphs endpoint for GET and POST request. +- The GET /graphs request fetches a list of graphs from GraphSpace matching the query parameters. +- The POST /graphs request creates a graph in GraphSpace. + +### GET /graphs +- The URL is the first thing that we would be setting for a request. We will set the URL to http://www.graphspace.org/api/v1/graphs. +![Rest API get](_static/images/rest-api/gs_rest_get_1.jpg) +- Provide Authorization: Select ‘Basic Auth’ from Authorization type drop-down. Enter the username and password and click on ‘Update Request’. +![Rest API get 2](_static/images/rest-api/gs_rest_get_2.jpg) +- Set Header: Add the following key value pairs, ```Content-Type:application/json and Accept:application/json.``` +![REST API get 3](_static/images/rest-api/gs_rest_get_3.jpg) +- Select Method: Changing the method is straightforward. Just select the method from the select control. We will use GET method here. +- Add URL Params: Clicking on the URL Params button will open up the key-value editor for entering URL parameters. The details of the URL Params for /graphs endpoint can be found in the [documentation](http://manual.graphspace.org/en/latest/Programmers_Guide.html#api-reference). +- Click on the Send button to the send the request. A list of graphs matching the query parameters will be received in the response. + +### POST /graphs +- The initial steps of setting URL, Authorization and Header are performed. +- Change Method to POST. +- Set Request Body: Click on Body to open the request body editor. Select raw request from the choices and JSON(application/json) from the drop-down. Enter the json data for the graph to be created in the editor. The details regarding the properties of the json graph body can be found in the [documentation](http://manual.graphspace.org/en/latest/Programmers_Guide.html#api-reference). +![REST API post 1](_static/images/rest-api/gs_rest_post_1.jpg) +- Click on the Send button to the send the request. A new graph object will be created and returned in the response. + +### Postman Collection + +A collection lets you group individual requests together. These requests can be further organized into folders to accurately mirror our API. Requests can also store sample responses when saved in a collection. You can add metadata like name and description too so that all the information that a developer needs to use your API is available easily. Collections can be exported as JSON files. Exporting a collection also saves the Authorization details. Hence, it is advised to remove the Authorization details from the Header before exporting. + +For quick use of the GraphSpace REST APIs or if you are stuck somewhere and you want reference, you can [download the collection of the APIs here](https://gist.github.com/sandeepm96/a824a6d0e643811389a6bf212e30a381). The collection has details regarding the API endpoints like params and body properties. Importing steps: +- Click Import button in the top menu. +- Choose the Import File in the pop up window. +![post man collection](_static/images/rest-api/post_man_collection.jpg) +- Provide the Authorization details for the imported requests (as Authorization details have been removed for security concern) + diff --git a/docs/_static/images/rest-api/gs_rest_get_1.jpg b/docs/_static/images/rest-api/gs_rest_get_1.jpg new file mode 100644 index 00000000..c0c56450 Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_1.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_get_2.jpg b/docs/_static/images/rest-api/gs_rest_get_2.jpg new file mode 100644 index 00000000..20e4355d Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_2.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_get_3.jpg b/docs/_static/images/rest-api/gs_rest_get_3.jpg new file mode 100644 index 00000000..fc2c7d2a Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_get_3.jpg differ diff --git a/docs/_static/images/rest-api/gs_rest_post_1.jpg b/docs/_static/images/rest-api/gs_rest_post_1.jpg new file mode 100644 index 00000000..cecbea69 Binary files /dev/null and b/docs/_static/images/rest-api/gs_rest_post_1.jpg differ diff --git a/docs/_static/images/rest-api/post_man_collection.jpg b/docs/_static/images/rest-api/post_man_collection.jpg new file mode 100644 index 00000000..9e235817 Binary files /dev/null and b/docs/_static/images/rest-api/post_man_collection.jpg differ diff --git a/graphspace/exceptions/error_codes.py b/graphspace/exceptions/error_codes.py index e229e976..05fc62f7 100644 --- a/graphspace/exceptions/error_codes.py +++ b/graphspace/exceptions/error_codes.py @@ -1,29 +1,38 @@ class ErrorCodes(object): - """ - A set of constants representing errors. Error messages can change, but the codes will not. - See the source for a list of all errors codes. - Codes can be used to check for specific errors. - """ - class Validation(object): - UserAlreadyExists = (1000, "User with `{0}` email id already exists!") + """ + A set of constants representing errors. Error messages can change, but the codes will not. + See the source for a list of all errors codes. + Codes can be used to check for specific errors. + """ + class Validation(object): + UserAlreadyExists = (1000, "User with `{0}` email id already exists!") - MethodNotAllowed = (1001, "Incoming request is not allowed") - BadRequest = (1002, "Bad Request") - UserPasswordMisMatch = (1003, "User/Password not recognized") - UserNotAuthorized = (1004, "You are not authorized to access this resource, create an account and contact resource's owner for permission to access this resource.") - UserNotAuthenticated = (1005, "User authentication failed") + MethodNotAllowed = (1001, "Incoming request is not allowed") + BadRequest = (1002, "Bad Request") + UserPasswordMisMatch = (1003, "User/Password not recognized") + UserNotAuthorized = ( + 1004, "You are not authorized to access this resource, create an account and contact resource's owner for permission to access this resource.") + UserNotAuthenticated = (1005, "User authentication failed") - # Graphs API - IsPublicNotSet = (1006, "`is_public` is required to be set to True when `owner_email` and `member_email` are not provided.") - NotAllowedGraphAccess = (1007, "User is not authorized to access private graphs created by {0}.") - CannotCreateGraphForOtherUser = (1008, "Cannot create graph with owner email = `{0}`.") - GraphIDMissing = (1009, "Graph ID is missing.") + # Graphs API + IsPublicNotSet = ( + 1006, "`is_public` is required to be set to True when `owner_email` and `member_email` are not provided.") + NotAllowedGraphAccess = ( + 1007, "User is not authorized to access private graphs created by {0}.") + CannotCreateGraphForOtherUser = ( + 1008, "Cannot create graph with owner email = `{0}`.") + GraphIDMissing = (1009, "Graph ID is missing.") - # Groups API - NotAllowedGroupAccess = (1010, "User is not authorized to access groups they aren't part of. Set `owner_email` or `member_email` to {0}.") - CannotCreateGroupForOtherUser = (1011, "Cannot create group with owner email = `{0}`.") + # Groups API + NotAllowedGroupAccess = ( + 1010, "User is not authorized to access groups they aren't part of. Set `owner_email` or `member_email` to {0}.") + CannotCreateGroupForOtherUser = ( + 1011, "Cannot create group with owner email = `{0}`.") - # Layouts API - NotAllowedLayoutAccess = (1012, "User is not authorized to access layouts which are not shared. Set `owner_email` to {0} or `is_shared` to 1.") - CannotCreateLayoutForOtherUser = (1013, "Cannot create layout with owner email = `{0}`.") - LayoutNameAlreadyExists = (1014, "Layout with name `{0}` already exists.") + # Layouts API + NotAllowedLayoutAccess = ( + 1012, "User is not authorized to access layouts which are not shared. Set `owner_email` to {0} or `is_shared` to 1.") + CannotCreateLayoutForOtherUser = ( + 1013, "Cannot create layout with owner email = `{0}`.") + LayoutNameAlreadyExists = ( + 1014, "Layout with name `{0}` already exists.") diff --git a/graphspace/middleware.py b/graphspace/middleware.py index e2989a59..317a9d96 100644 --- a/graphspace/middleware.py +++ b/graphspace/middleware.py @@ -3,65 +3,81 @@ from django.http import HttpResponseRedirect, HttpResponse, QueryDict import json from graphspace.exceptions import * +from graphspace.exceptions import ErrorCodes, GraphSpaceError class SQLAlchemySessionMiddleware(object): - def process_request(self, request): - request.db_session = settings.db.session() + def process_request(self, request): + request.db_session = settings.db.session() - def process_response(self, request, response): - try: - session = request.db_session - except AttributeError: - return response + def process_response(self, request, response): + try: + session = request.db_session + except AttributeError: + return response - try: - session.commit() - session.close() - return response - except: - session.rollback() - session.close() - raise + try: + session.commit() + session.close() + return response + except: + session.rollback() + session.close() + raise - def process_exception(self, request, exception): - try: - session = request.db_session - except AttributeError: - return - session.rollback() - session.close() + def process_exception(self, request, exception): + try: + session = request.db_session + except AttributeError: + return + session.rollback() + session.close() class GraphSpaceMiddleware(object): - def process_request(self, request): - request.session['uid'] = request.session['uid'] if 'uid' in request.session else None + def process_request(self, request): + request.session['uid'] = request.session['uid'] if 'uid' in request.session else None - def process_response(self, request, response): - return response + def process_response(self, request, response): + if settings.MAINTENANCE: + if 'application/json' in request.META.get('HTTP_ACCEPT', None): + return HttpResponse( + str(GraphSpaceError( + status=500, + uri=request.path, + msg="GraphSpace is undergoing scheduled maintenance. Please check back soon.", code=2000 + )), + content_type="application/json", status=500) + else: + return render(request, 'maintenance.html') + else: + return response - def process_exception(self, request, exception): - # TODO: Handle different types of error - if request.META.get('HTTP_ACCEPT', None) == 'application/json': - if issubclass(type(exception), GraphSpaceError): - return HttpResponse(str(exception), content_type="application/json", status=exception.status) + def process_exception(self, request, exception): + # TODO: Handle different types of error + if request.META.get('HTTP_ACCEPT', None) == 'application/json': + if issubclass(type(exception), GraphSpaceError): + return HttpResponse(str(exception), content_type="application/json", status=exception.status) - if exception.message == 'Unauthenticated': - response = HttpResponse(content_type="application/json", status=401) - response['WWW-Authenticate'] = 'Basic' - return response - elif exception.message == 'Unauthorized': - response = HttpResponse(content_type="application/json", status=403) - response['WWW-Authenticate'] = 'Basic' - return response - else: - return HttpResponse(str(BadRequest(request, msg=str(exception))), content_type="application/json", status=400) + if exception.message == 'Unauthenticated': + response = HttpResponse( + content_type="application/json", status=401) + response['WWW-Authenticate'] = 'Basic' + return response + elif exception.message == 'Unauthorized': + response = HttpResponse( + content_type="application/json", status=403) + response['WWW-Authenticate'] = 'Basic' + return response + else: + return HttpResponse(str(BadRequest(request, msg=str(exception))), content_type="application/json", status=400) - context = exception.to_dict() if issubclass(type(exception), GraphSpaceError) else {} + context = exception.to_dict() if issubclass( + type(exception), GraphSpaceError) else {} - if exception.message == 'Unauthenticated': - context['message'] = 'You are not authenticated to view this page.' - elif exception.message == 'Unauthorized': - context['message'] = 'You are not authorized to view this page.' + if exception.message == 'Unauthenticated': + context['message'] = 'You are not authenticated to view this page.' + elif exception.message == 'Unauthorized': + context['message'] = 'You are not authorized to view this page.' - return render(request, '500.html', context) + return render(request, '500.html', context) diff --git a/graphspace/settings/base.py b/graphspace/settings/base.py index 526d7469..a247f43b 100644 --- a/graphspace/settings/base.py +++ b/graphspace/settings/base.py @@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ALLOWED_HOSTS = ['*'] -APPEND_SLASH=True +APPEND_SLASH = True # GLOBAL VALUES FOR DATABASE DB_FULL_PATH = os.path.join(BASE_DIR, 'graphspace.db') @@ -25,25 +25,25 @@ # Application definition INSTALLED_APPS = ( - 'analytical', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'applications.users', - 'applications.graphs' + 'analytical', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'applications.users', + 'applications.graphs' ) MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.middleware.common.CommonMiddleware', - 'graphspace.middleware.SQLAlchemySessionMiddleware', - 'graphspace.middleware.GraphSpaceMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.middleware.common.CommonMiddleware', + 'graphspace.middleware.SQLAlchemySessionMiddleware', + 'graphspace.middleware.GraphSpaceMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', ) ROOT_URLCONF = 'graphspace.urls' @@ -98,26 +98,26 @@ STATIC_URL = '/static/' STATICFILES_DIRS = ( - os.path.join(BASE_DIR, "static"), + os.path.join(BASE_DIR, "static"), ) TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, "templates")], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'graphspace.context_processors.auth', - 'graphspace.context_processors.static_urls', - 'graphspace.context_processors.login_forms', - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, "templates")], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'graphspace.context_processors.auth', + 'graphspace.context_processors.static_urls', + 'graphspace.context_processors.login_forms', + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] @@ -128,13 +128,13 @@ # Following the recommendation of the Django tutorial at PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher', ) BASE = declarative_base() @@ -151,10 +151,12 @@ }, }, 'loggers': { - 'applications': { + 'applications': { 'handlers': ['file'], 'level': 'DEBUG', 'propagate': True, }, }, -} \ No newline at end of file +} + +MAINTENANCE = False diff --git a/graphspace/settings/local.py b/graphspace/settings/local.py index 9b2fa36d..b15da20d 100644 --- a/graphspace/settings/local.py +++ b/graphspace/settings/local.py @@ -8,6 +8,7 @@ # If error is thrown, display the error in the browser (ONLY FOR LOCAL MACHINES) DEBUG = True TEMPLATE_DEBUG = True +MAINTENANCE = False # URL through which to access graphspace URL_PATH = "http://localhost:8000/" @@ -45,4 +46,4 @@ 'HOST': 'localhost', 'PORT': '5432' } -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index b75a2132..1f6c5be8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-cors-middleware==1.3.1 django-oauth-toolkit==0.11.0 docutils==0.12 imagesize==0.7.1 -Jinja2==2.8 +Jinja2==2.10 MarkupSafe==0.23 networkx==1.11 oauthlib==1.1.2 @@ -20,7 +20,7 @@ pytz==2016.4 requests==2.10.0 six==1.10.0 snowballstemmer==1.2.1 -SQLAlchemy==1.0.14 +SQLAlchemy==1.3.3 graphspace_python==0.8.2 enum34 elasticsearch>=5.0.0,<6.0.0 diff --git a/static/css/graphspace.css b/static/css/graphspace.css index ff68bde3..49bf1594 100644 --- a/static/css/graphspace.css +++ b/static/css/graphspace.css @@ -261,6 +261,22 @@ ul.nav.nav-tabs > li > a:hover { border-left: 5px double rgba(36, 41, 46, 0.21); } +#compare-sidebar { + z-index: 1000; + position: absolute; + right: 250px; + width: 250px; + height: 100%; + margin-right: -250px; + overflow-y: scroll; + background: transparent; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; + border-left: 5px double rgba(36, 41, 46, 0.21); +} + #wrapper.toggled #sidebar-wrapper { width: 250px; } @@ -438,4 +454,24 @@ p.lead { position: relative; margin-left: 0; } +} + +.compare-table-td { + margin-right:15px; +} + +#search-place-holder { + padding: 20px; + margin-top: -6px; + width: 100%; + border: 0; + border-radius: 0; + background: #f1f1f1; +} + +#parameter-header { + text-align: left; + padding: 20px 30px 25px 20px; + border: 1px solid #e1e4e5; + border-radius: 3px; } \ No newline at end of file diff --git a/static/js/graphs_compare.js b/static/js/graphs_compare.js new file mode 100644 index 00000000..4ed9c712 --- /dev/null +++ b/static/js/graphs_compare.js @@ -0,0 +1,602 @@ +/** + * Created by jahan on 10/07/19. + */ + +var compareGraphPage = { + cyGraph: undefined, + graph_ids: [], + graphs_json: [], + styles_json: [], + common_nodes: undefined, + edge_name_to_id: {}, + timeout: null, + init: function () { + /** + * This function is called to setup the upload graph page. + * It will initialize all the event listeners. + */ + + $('#nodes-li').hide(); + $('#edges-li').hide(); + $('#visualization-li').hide(); + compareGraphPage.loadGraphs(); + $('#graphVisualizationTabBtn').click(function (e) { + window.setTimeout(function () { + $('#cyGraphContainer').css('height', '99%'); + }, 100); + compareGraphPage.cyGraph.fit().center(); + }); + $("#search-place-holder").on("keyup", function () { + var value = $(this).val().toLowerCase(); + $(".dropdown-menu li").filter(function () { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + }); + }); + $("#dropdownMenu1").on("focusin", function () { + var value = ""; + $("#search-place-holder").val(""); + $(".dropdown-menu li").filter(function () { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + }); + }); + $("#dropdownMenu2").on("focusin", function () { + var value = ""; + $("#search-place-holder").val(""); + $(".dropdown-menu li").filter(function () { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + }); + }); + $('#colorpicker1').colorpicker(); + $('#colorpicker1').colorpicker().on('changeColor', function (event) { + $('#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()); + } + }); + $('#colorpicker2').colorpicker(); + $('#colorpicker2').colorpicker().on('changeColor', function (event) { + $('#colorpicker2').css('background-color', event.color.toString()); + $('#colorpicker2').val(event.color.toString()); + if (compareGraphPage.cyGraph) { + compareGraphPage.setNodesColor('graph_2', event.color.toString()); + compareGraphPage.setNodesColor('common_1', $('#operatorcolorpicker').val()); + } + }); + $('#operatorcolorpicker').colorpicker(); + $('#operatorcolorpicker').colorpicker().on('changeColor', function (event) { + $('#operatorcolorpicker').css('background-color', event.color.toString()); + $('#operatorcolorpicker').val(event.color.toString()); + compareGraphPage.setNodesColor('common_1', event.color.toString()); + }); + if ( graph_1_id && graph_2_id && operation ){ + $('#dropdownMenu1').attr('value', graph_1_id); + $('#dropdownMenu2').attr('value', graph_2_id); + $('#operatorMenu1').attr('value', operation); + $('#operatorMenu1').parent().find('a[row_id="'+ operation +'"]').click(); + } + + }, + validateExpression: function (infix) { + var balance = 0; + // remove white spaces to simplify regex + infix = infix.replace(/ /g, ''); + var regex = /[\+\-]?\w+(([\+\-\*\/\&\|\!]|(\<\=?|\>\=?|\=\=|\!=))[\+\-]?\w+)*/; + + // if it has empty parenthesis then is not valid + if (infix.match(/\(\)/)) { + return false; + } + + // validate parenthesis balance + for (var i = 0; i < infix.length; i++) { + if (infix[i] == '(') { + balance++; + } else if (infix[i] == ')') { + balance--; + } + + if (balance < 0) { + return false; + } + } + + if (balance > 0) { + return false; + } + + // remove all the parenthesis + infix = infix.replace(/[\(\)]/g, ''); + + return infix.match(regex)[0] == infix; + }, + convertToPostfix: function () { + + }, + loadGraphs: function () { + var params = {'data': {'sort': 'updated_at', 'order': 'desc', 'offset': 0, 'limit': 10}}; + query = ''; + params.data["owner_email"] = $('#UserEmail').val(); + + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + delete params.data.owner_email; + params.data["member_email"] = $('#UserEmail').val(); + + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + delete params.data.member_email; + + params.data["is_public"] = 1; + apis.graphs.search(params.data, query, + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.populateCompareDropdownMenu(response['graphs']); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + setDropdownMenu: function (obj) { + var label = obj.parent().parent().siblings('button').children('i'); + if (label.text()) { + label.text(label.attr('value')); + obj.parent().parent().siblings('button').children('bold').text(obj.attr('data')); + } else { + obj.parent().parent().siblings("button[class*='dropdown-toggle']").text(obj.attr('data')); + obj.parent().parent().siblings("button[class*='dropdown-toggle']").append(''); + compareGraphPage.compareGraphs(); + } + obj.parent().parent().siblings("button[class*='dropdown-toggle']").attr("value", obj.attr('row_id')); + }, + populateCompareDropdownMenu: function (data) { + $.each(data, function (i, item) { + if ($('#UserEmail').val() == item.owner_email && $(".compare-dropdown[mygraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('mygraphs', 'true'); + } else if (item.owner_email.startsWith("public") && $(".compare-dropdown[publicgraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('publicgraphs', 'true'); + } else if ($('#UserEmail').val() != item.owner_email && !item.owner_email.startsWith("public") && $(".compare-dropdown[sharedgraphs='false']").length) { + $(".compare-dropdown").append(''); + $(".compare-dropdown").append(''); + $(".compare-dropdown").attr('sharedgraphs', 'true'); + } + $(".compare-dropdown").append('
  • ' + + item.name + '
  • ') + }); + }, + setNodesColor: function (graph_parent, color) { + 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({'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'}); + }, + setGraph0Groups: function (graph_json, common_nodes) { + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + graph_json['elements']['nodes'][len] = { + 'data': { + 'id': 'graph_1', + 'label': 'Graph 1', + 'name': 'Graph 1' + } + }; + graph_json['elements']['nodes'][len + 1] = {'data': {'id': 'common_1', 'label': 'Common', 'name': 'Common'}}; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) + item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; + _.each(common_nodes, function (innerNode) { + if (innerNode.length) + innerNode = (innerNode[0]['graph_id'] == compareGraphPage.graph_1_id) ? innerNode[0] : innerNode[1]; + if (item['data']['label'] == innerNode['label']) { + if (!common_id) { + common_id = item['data']['id']; + compareGraphPage.common_nodes['parent1'] = common_id; + } + item['data']['parent'] = 'common_1'; + } + }); + }); + }, + setGraph1Groups: function (graph_json, common_nodes) { + const len = graph_json['elements']['nodes'].length; + var common_id = undefined; + var duplicate_nodes = []; + graph_json['elements']['nodes'][len] = { + 'data': { + 'id': 'graph_2', + 'label': 'Graph 2', + 'name': 'Graph 2' + } + }; + _.each(graph_json['elements']['nodes'], function (item) { + if (item['data']['id'] != graph_json['elements']['nodes'][len]['data']['id']) + item['data']['parent'] = graph_json['elements']['nodes'][len]['data']['id']; + _.each(common_nodes, function (innerNode) { + if (innerNode.length) + innerNode = (innerNode[0]['graph_id'] == compareGraphPage.graph_ids[1]) ? innerNode[0] : innerNode[1]; + if (item['data']['label'] == innerNode['label']) { + duplicate_nodes.push(item); + } + }); + }); + _.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'; + }, + compareGraphs: function () { + graph_1_id = compareGraphPage.graph_ids[0] = $('#dropdownMenu1').attr('value'); + graph_2_id = compareGraphPage.graph_ids[1] = $('#dropdownMenu2').attr('value'); + + operation = $('#operatorMenu1').attr('value'); + apis.graphs.getByID(compareGraphPage.graph_ids[0], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[0] = response['graph_json']; + compareGraphPage.styles_json[0] = response['style_json']; + apis.graphs.getByID(compareGraphPage.graph_ids[1], + successCallback = function (response) { + // This method is called when graphs are successfully fetched. + compareGraphPage.graphs_json[1] = response['graph_json']; + compareGraphPage.styles_json[1] = response['style_json']; + compareGraphPage.compareGraphHelper(); + $('#dropdownMenu1').parent().find('a[row_id="'+ graph_1_id +'"]').click(); + $('#dropdownMenu2').parent().find('a[row_id="'+ graph_2_id +'"]').click(); + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + + }, + errorCallback = function () { + // This method is called when error occurs while fetching graphs. + params.error('Error'); + } + ); + }, + formatCyGraph: function () { + + compareGraphPage.cyGraph.filter(":parent").style({ + 'font-size': '0px', + }); + compareGraphPage.cyGraph.filter(":edges").style({ + 'text-outline-color': '#FFFFFF', + 'line-color': '#000000' + }); + compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").layout({ + 'name': 'grid', + 'animate': false, + 'padding': 10 + }).run(); + compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").layout({ + 'name': 'grid', + 'animate': false, + 'padding': 10 + }).run(); + + compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").layout({ + 'name': 'grid', + 'animate': false, + 'padding': 10 + }).run(); + + let x_max = compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").max(function (ele, i, eles) { + return ele.position()['x']; + }); + let y_max = compareGraphPage.cyGraph.filter("node[parent='" + 'graph_1' + "']").max(function (ele, i, eles) { + return ele.position()['y']; + }); + y_max = Math.max(y_max.value, compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").max(function (ele, i, eles) { + return ele.position()['y']; + }).value); + + let cm_len_1 = compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").max(function (ele, i, eles) { + return ele.position()['x']; + }).value - compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").min(function (ele, i, eles) { + return ele.position()['x']; + }).value; + + compareGraphPage.cyGraph.filter("node[parent='" + 'graph_2' + "']").layout({ + 'name': 'grid', 'animate': false, 'padding': 10, transform: (node) => { + let position = {}; + position.x = node.position('x') + x_max.value + 100; + position.y = node.position('y'); + return position; + } + }).run(); + + compareGraphPage.cyGraph.filter("node[parent='" + 'common_1' + "']").layout({ + 'name': 'grid', 'animate': false, 'padding': 10, transform: (node) => { + let position = {}; + position.x = node.position('x') + Math.abs(cm_len_1 / 2 - x_max.value) + 100; + position.y = node.position('y') + y_max + 150; + return position; + } + }).run(); + }, + CommonElementsHelper: function(nodes){ + + }, + setCommonElements: function (nodes) { + $.each(nodes, function (i, node) { + if (node.length > 1) { + let id_1 = compareGraphPage.cyGraph.nodes("[label = '" + node[0]['label'] + "']").id(); + let id_2 = compareGraphPage.cyGraph.nodes("[label = '" + node[1]['label'] + "']").id(); + + edges = compareGraphPage.cyGraph.nodes("[id = '" + id_1 + "']").connectedEdges(); + $.each(edges, function (i, edge) { + if (edge.data('source') == id_1) { + edge.move({source: id_2}); + } else + edge.move({target: id_2}); + }); + compareGraphPage.cyGraph.remove("node[id = '" + id_1 + "']"); + } + }); + + }, + compareGraphHelper: function () { + operation = $('#operatorMenu1').attr('value'); + if (operation && compareGraphPage.graph_ids[0] && compareGraphPage.graph_ids[1]) { + $('#nodes-table > thead').find("th").remove(); + $('#edges-table > thead').find("th").remove(); + $('#nodes-table > thead > tr').append('

    Graph 1

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

    Graph 2

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

    Graph 1

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

    Graph 2

    '); + + apis.compare.get({ + 'graph_1_id': compareGraphPage.graph_ids[0], + 'graph_2_id': compareGraphPage.graph_ids[1], + 'operation': operation + }, + successCallback = function (response) { + // $('#nodes-table').DataTable(); + compareGraphPage.common_nodes = response['nodes']; + + + compareGraphPage.setGraph0Groups(compareGraphPage.graphs_json[0], response['nodes']); + compareGraphPage.setGraph1Groups(compareGraphPage.graphs_json[1], response['nodes']); + compareGraphPage.graphs_json[0]['elements']['nodes'] = compareGraphPage.graphs_json[0]['elements']['nodes'].concat(compareGraphPage.graphs_json[1]['elements']['nodes']); + compareGraphPage.graphs_json[0]['elements']['edges'] = compareGraphPage.graphs_json[0]['elements']['edges'].concat(compareGraphPage.graphs_json[1]['elements']['edges']); + compareGraphPage.styles_json[0]['style'] = compareGraphPage.styles_json[0]['style'].concat(compareGraphPage.styles_json[1]['style']); + + compareGraphPage.cyGraph = compareGraphPage.constructCytoscapeGraph(compareGraphPage.graphs_json[0], compareGraphPage.styles_json[0]); + + compareGraphPage.populateNodeData(response['nodes']); + compareGraphPage.populateEdgeData(response['edges']); + // compareGraphPage.setCommonElements(response['nodes']); + compareGraphPage.cyGraph.ready(function () { // Wait for cytoscape to actually load and map eles + compareGraphPage.cyGraph.nodes().forEach(function (ele) { // Your function call inside + console.log("loop", ele.id(), ele.position()); + }); + compareGraphPage.formatCyGraph(); + compareGraphPage.cyGraph.panzoom(); + compareGraphPage.cyGraph.reset().fit().center(); + }); + + $('#nodes-total-badge').text(response['nodes'].length); + $('#edges-total-badge').text(response['edges'].length); + + compareGraphPage.setNodesColor('graph_1', $('#colorpicker1').val()); + compareGraphPage.setNodesColor('graph_2', $('#colorpicker2').val()); + + compareGraphPage.setNodesColor('common_1', $('#operatorcolorpicker').val()); + + $('#nodes-table').DataTable().draw(); + $('#edges-table').DataTable().draw(); + $('.dataTables_length').addClass('bs-select'); + + $('#nodes-li').show(); + $('#visualization-li').show(); + $('#edges-li').show(); + $('#visualization-li a:last').tab('show'); + $('#visualization-li a:first').tab('show'); + }, + errorCallback = function (xhr, status, errorThrown) { + // This method is called when error occurs while deleting group_to_graph relationship. + $.notify({message: "You are not authorized to access one or more graphs selected for comparison."}, {type: 'danger'}); + }); + } else { + $.notify({message: "Please select correct parameters for Graph comparison."}, {type: 'danger'}); + } + }, + resetData: function () { + location.replace("/compare"); + + }, + populateNodeData: function (nodes) { + var trHTML = ''; + var cyNode1 = undefined; + var cyNode2 = undefined; + $('#nodes-table').DataTable().clear().destroy(); + $('#nodes-comparison-table').find("tr:gt(0)").remove(); + if (nodes.length && !nodes[0].length) { + $('#nodes-table > thead').find("th:gt(0)").remove(); + $('#nodes-table').parent().attr('align', 'center'); + $('#nodes-table').attr('style', 'width:800px;'); + + } else $('#nodes-table').attr('style', ''); + $.each(nodes, function (i, item) { + if (item.length) { + // Use 'name' field instead - for testing use 'label' + // cyNode1 = compareGraphPage.cyGraph.getElementById(item[0]['label']); + // cyNode2 = compareGraphPage.cyGraph.getElementById(item[1]['label']); + + cyNode1 = compareGraphPage.cyGraph.nodes("[label = '" + item[0]['label'] + "']"); + cyNode2 = compareGraphPage.cyGraph.nodes("[label = '" + item[1]['label'] + "']"); + + trHTML += 'Name : ' + item[0]['name'] + + '
    Label : ' + item[0]['label']; + if (cyNode1.length && cyNode1.data() && cyNode1.data()['popup']) { + trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ' Name : ' + item[1]['name'] + + '
    Label : ' + item[1]['label']; + + if (cyNode2.length && cyNode2.data() && cyNode2.data()['popup']) { + trHTML += '
    ' + cyNode2.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + + } else { + // Use 'name' field instead - for testing use 'label' + cyNode1 = compareGraphPage.cyGraph.getElementById(item['label']); + + trHTML += 'Name : ' + item['name'] + + '
    Label : ' + item['label']; + + if (cyNode1.length && cyNode1.data() && cyNode1.data()['popup']) { + trHTML += '
    ' + cyNode1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + } + + }); + $('#nodes-comparison-table').append(trHTML); + }, + populateEdgeData: function (edges) { + var trHTML = ''; + var cyEdge1 = undefined; + var cyEdge2 = undefined; + $('#edges-table').DataTable().clear().destroy(); + $('#edges-comparison-table').find("tr:gt(0)").remove(); + if (edges.length && !edges[0].length) { + $('#edges-table > thead').find("th:gt(0)").remove(); + $('#edges-table').parent().attr('align', 'center'); + $('#edges-table').attr('style', 'width:800px;'); + } else $('#edges-table').attr('style', ''); + $.each(edges, function (i, item) { + + if (item.length) { + cyEdge1 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item[0]['name']]); + cyEdge2 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item[1]['name']]); + + trHTML += 'Name : ' + item[0]['name'] + + '
    Label : ' + item[0]['label']; + + if (cyEdge1.length && cyEdge1.data() && cyEdge1.data()['popup']) { + trHTML += '
    ' + cyEdge1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ' Name : ' + item[1]['name'] + + '
    Label : ' + item[1]['label']; + + if (cyEdge2.length && cyEdge2.data() && cyEdge2.data()['popup']) { + trHTML += '
    ' + cyEdge2.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + + trHTML += ''; + + } else { + cyEdge1 = compareGraphPage.cyGraph.getElementById(compareGraphPage.edge_name_to_id[item['name']]); + + trHTML += 'Name : ' + item['name']; + + if (cyEdge1.length && cyEdge1.data() && cyEdge1.data()['popup']) { + trHTML += '
    ' + cyEdge1.data()['popup'].replace(/<\s*hr\s*\/>/gi, ''); + } + trHTML += ''; + } + }); + $('#edges-comparison-table').append(trHTML); + }, + constructCytoscapeGraph: function (graph_json, style_json) { + + layout = { + name: 'grid', + padding: 20, + fit: true, + animate: false + }; + + graph_json['elements']['nodes'] = _.map(graph_json['elements']['nodes'], function (node) { + var newNode = { + "data": node['data'] + }; + if ('position' in node) { + newNode['position'] = node['position']; + layout = { + name: 'preset' + }; + } + return newNode + }); + + graph_json['elements']['edges'] = _.map(graph_json['elements']['edges'], function (edge) { + compareGraphPage.edge_name_to_id[edge['data']['name']] = edge['data']['id']; + return { + "data": edge['data'] + } + }); + + return cytoscape({ + container: document.getElementById('cyGraphContainer'), + boxSelectionEnabled: true, + autounselectify: false, + wheelSensitivity: 0.2, + minZoom: 1e-2, + maxZoom: 1e2, + elements: graph_json['elements'], + layout: layout, + + //Style properties of NODE body + style: _.concat(defaultStylesheet, cytoscapeGraph.parseStylesheet(style_json), selectedElementsStylesheet), + + ready: function () { + + //setup popup dialog for displaying dialog when nodes/edges + //are clicked for information. + + $('#dialog').dialog({ + autoOpen: false + }); + // this.layout({ + // 'name': 'grid', + // 'animate': false, + // }).run(); + // this.reset(); + // this.fit(); + // this.center(); + // display node data as a popup + this.on('tap', graphPage.onTapGraphElement); + + } + }); + + }, +}; \ No newline at end of file diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index a7f4c57a..f16eb165 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -63,6 +63,12 @@ var apis = { apis.jsonRequest('DELETE', apis.layouts.ENDPOINT({'graph_id': graph_id}) + layout_id, undefined, successCallback, errorCallback) } }, + compare: { + ENDPOINT: '/ajax/compare/', + get: function (data, successCallback, errorCallback) { + apis.jsonRequest('GET', apis.compare.ENDPOINT, data, successCallback, errorCallback) + } + }, logging: { ENDPOINT: _.template('http://<%= hostname %>:9200/layouts/action'), add: function (data, successCallback, errorCallback) { @@ -427,6 +433,8 @@ var uploadGraphPage = { } }; + + var graphPage = { cyGraph: undefined, timeout: null, @@ -452,9 +460,6 @@ var graphPage = { }); $('#saveLayoutBtn').click(function () { - - cytoscapeGraph.showGraphInformation(graphPage.cyGraph); - graphPage.saveLayout($('#saveLayoutNameInput').val(), '#saveLayoutModal'); }); @@ -500,7 +505,6 @@ var graphPage = { window.setTimeout(function () { $('#cyGraphContainer').css('height', '99%'); }, 100); - }); if (utils.getURLParameter('query')) { @@ -601,10 +605,10 @@ var graphPage = { }); layoutID.positions = corrected_positions; } - graphPage.cyGraph.layout(layoutID); + graphPage.cyGraph.layout(layoutID).run(); }, - saveLayout: function (layoutName, modalNameId) { + saveLayout: function (layoutName, modalNameId, callback) { graphPage.cyGraph.elements().unselect(); if (_.trim(layoutName).length === 0) { @@ -659,6 +663,11 @@ var graphPage = { $(modalNameId).modal('toggle'); $('#PrivateLayoutsTable').bootstrapTable('refresh'); $('#SharedLayoutsTable').bootstrapTable('refresh'); + $('table').bootstrapTable('refresh'); + if(typeof callback === 'function'){ + callback(response.id); + }; + }, errorCallback = function (response) { // This method is called when error occurs while deleting group_to_graph relationship. @@ -870,7 +879,7 @@ var graphPage = { }, onTapGraphElement: function (evt) { // get target - var target = evt.cyTarget; + var target = evt.target; // target some element other than background (node/edge) if (target !== this) { var popup = target._private.data.popup; @@ -1049,10 +1058,13 @@ var graphPage = { }, defaultLayoutWidget: { init: function (is_shared) { - if (utils.getURLParameter('auto_layout') || _.isNil(is_shared)) { + if (_.isNil(is_shared)) { $('#setDefaultLayoutBtn').hide(); $('#removeDefaultLayoutBtn').hide(); - } else if (utils.getURLParameter('user_layout') && utils.getURLParameter('user_layout') == default_layout_id) { + }else if (utils.getURLParameter('auto_layout')) { + $('#setDefaultLayoutBtn').show(); + $('#removeDefaultLayoutBtn').hide(); + }else if (utils.getURLParameter('user_layout') && utils.getURLParameter('user_layout') == default_layout_id) { $('#setDefaultLayoutBtn').hide(); $('#removeDefaultLayoutBtn').show(); } else { @@ -1060,8 +1072,31 @@ var graphPage = { $('#removeDefaultLayoutBtn').hide(); } }, - onSetDefaultLayoutBtn: function (e) { + setDefaultAutoLayoutBtn: function (layout_id) { apis.graphs.update($('#GraphID').val(), { + 'default_layout_id': layout_id + }, + successCallback = function (response) { + default_layout_id = layout_id; + graphPage.defaultLayoutWidget.init(1); + $('#setDefaultLayoutBtn').hide(); + $('#removeDefaultLayoutBtn').show(); + }, + errorCallback = function (xhr, status, errorThrown) { + $.notify({ + message: response.responseJSON.error_message + }, { + type: 'danger' + }); + }); + }, + onSetDefaultLayoutBtn: function (e) { + if (utils.getURLParameter('auto_layout')) { + graphPage.saveLayout(utils.getURLParameter('auto_layout'), utils.getURLParameter('auto_layout'), + graphPage.defaultLayoutWidget.setDefaultAutoLayoutBtn); + + } else { + apis.graphs.update($('#GraphID').val(), { 'default_layout_id': utils.getURLParameter('user_layout') }, successCallback = function (response) { @@ -1076,6 +1111,7 @@ var graphPage = { type: 'danger' }); }); + } }, onRemoveDefaultLayoutBtn: function (e) { apis.graphs.update($('#GraphID').val(), { @@ -1386,7 +1422,7 @@ var graphPage = { graphPage.cyGraph.on('free', function (e) { - var selected_elements = e.cyTarget.length > 1 ? graphPage.cyGraph.elements(':selected') : e.cyTarget; + var selected_elements = e.target.length > 1 ? graphPage.cyGraph.elements(':selected') : e.target; graphPage.layoutEditor.undoRedoManager.update({ 'action_type': 'move_node', 'data': { @@ -1489,7 +1525,7 @@ var graphPage = { } }); - graphPage.cyGraph.elements().on('select, unselect', function () { + graphPage.cyGraph.elements().on('select unselect', function () { if (graphPage.cyGraph.nodes(':selected').length > 0) { $('#editSelectedNodesBtn').removeClass('disabled'); } else { @@ -1969,43 +2005,6 @@ var graphPage = { } return largestK; }, - applyMax: function (graph_layout) { - //Gets all nodes and edges up do the max value set - //and only renders them - var maxVal = parseInt($("#input_max").val()); - - if (!maxVal) { - return; - } - var newJSON = { - "nodes": new Array(), - "edges": new Array() - }; - - // List of node ids that should remain in the graph - var nodeNames = Array(); - - //Get all edges that meet the max quantifier - for (var i = 0; i < graph_json.elements['edges'].length; i++) { - var edge_data = graph_json.elements['edges'][i]; - if (edge_data['data']['k'] <= maxVal) { - newJSON['edges'].push(edge_data); - nodeNames.push(edge_data['data']['source']); - nodeNames.push(edge_data['data']['target']); - } - } - - //Get all nodes that meet the max quantifier - for (var i = 0; i < graph_json.elements['nodes'].length; i++) { - var node_data = graph_json.elements['nodes'][i]; - if (nodeNames.indexOf(node_data['data']['id']) > -1) { - newJSON['nodes'].push(node_data); - } - } - - graphPage.cyGraph.load(newJSON); - graphPage.filterNodesEdges.showOnlyK(); - }, showOnlyK: function () { // Returns all the id's that are > k value if ($("#input_k").val()) { @@ -2338,43 +2337,6 @@ var graphPage = { max: 50 }); }, - applyMax: function (graph_layout) { - //Gets all nodes and edges up do the max value set - //and only renders them - var maxVal = parseInt($("#input_max").val()); - - if (!maxVal) { - return; - } - var newJSON = { - "nodes": new Array(), - "edges": new Array() - }; - - // List of node ids that should remain in the graph - var nodeNames = Array(); - - //Get all edges that meet the max quantifier - for (var i = 0; i < graph_json.elements['edges'].length; i++) { - var edge_data = graph_json.elements['edges'][i]; - if (edge_data['data']['k'] <= maxVal) { - newJSON['edges'].push(edge_data); - nodeNames.push(edge_data['data']['source']); - nodeNames.push(edge_data['data']['target']); - } - } - - //Get all nodes that meet the max quantifier - for (var i = 0; i < graph_json.elements['nodes'].length; i++) { - var node_data = graph_json.elements['nodes'][i]; - if (nodeNames.indexOf(node_data['data']['id']) > -1) { - newJSON['nodes'].push(node_data); - } - } - - graphPage.cyGraph.load(newJSON); - graphPage.filterNodesEdges.showOnlyK(); - }, setBarToValueEdgeLength: function (inputId, barId) { /** * If the user enters a value greater than the max value allowed, change value of bar to max allowed value. @@ -2443,7 +2405,7 @@ var cytoscapeGraph = { title: 'edit selected nodes', selector: 'node', onClickFunction: function (event) { - graphPage.layoutEditor.nodeEditor.open(cy.collection(cy.elements(':selected')).add(event.cyTarget).select()); + graphPage.layoutEditor.nodeEditor.open(cy.collection(cy.elements(':selected')).add(event.target).select()); }, hasTrailingDivider: true }, @@ -2453,7 +2415,7 @@ var cytoscapeGraph = { selector: 'node', show: true, onClickFunction: function (event) { - selectAllOfTheSameType(event.cyTarget); + selectAllOfTheSameType(event.target); } }, { @@ -2462,7 +2424,7 @@ var cytoscapeGraph = { selector: 'node', show: true, onClickFunction: function (event) { - unselectAllOfTheSameType(event.cyTarget); + unselectAllOfTheSameType(event.target); } }, { @@ -2471,7 +2433,7 @@ var cytoscapeGraph = { selector: 'edge', show: true, onClickFunction: function (event) { - selectAllOfTheSameType(event.cyTarget); + selectAllOfTheSameType(event.target); } }, { @@ -2480,7 +2442,7 @@ var cytoscapeGraph = { selector: 'edge', show: true, onClickFunction: function (event) { - unselectAllOfTheSameType(event.cyTarget); + unselectAllOfTheSameType(event.target); } } ] @@ -2784,7 +2746,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: false, padding: 0 - }); + }).run(); } else if (layout_name === "fill_circle") { collection.layout( { @@ -2792,7 +2754,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: false, padding: 40 - }); + }).run(); } else if (layout_name === "grid") { collection.layout( { @@ -2800,7 +2762,7 @@ var cytoscapeGraph = { fit: false, avoidOverlap: true, condense: true - }); + }).run(); } else if (layout_name === "square") { cytoscapeGraph.runSquareLayoutOnCollection(cy, collection); } else if (layout_name === "horizontal") { diff --git a/static/js/groups_page.js b/static/js/groups_page.js index be5f3c6e..b7e88453 100644 --- a/static/js/groups_page.js +++ b/static/js/groups_page.js @@ -72,6 +72,8 @@ var groupsPage = { submit: function (e) { e.preventDefault(); + $('#CreateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val(), @@ -79,7 +81,13 @@ var groupsPage = { }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#CreateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return; } apis.groups.add(group, @@ -89,7 +97,21 @@ var groupsPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while adding group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#CreateGroupBtn').attr('disabled', false); }); } }, @@ -282,13 +304,21 @@ var groupPage = { submit: function (e) { e.preventDefault(); + $('#UpdateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val() }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#UpdateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return } apis.groups.update($('#GroupID').val(), group, @@ -298,7 +328,21 @@ var groupPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while updating group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#UpdateGroupBtn').attr('disabled', false); }); } }, diff --git a/static/js/main.js b/static/js/main.js index aefeaf82..47fe3cd0 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -62,6 +62,18 @@ var header = { var password = $("#password").val(); var verify_password = $("#verify_password").val(); + if ($("#user_id")) { + var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + if (reg.test(user_id) == false) { + $.notify({ + message: 'Please enter a valid email address!' + }, { + type: 'warning' + }); + return; + } + } + if (!$("#user_id") || user_id.length == 0) { $.notify({ message: 'Please enter your email!' diff --git a/templates/compare_graph/compare_graphs.html b/templates/compare_graph/compare_graphs.html new file mode 100644 index 00000000..3e7e184d --- /dev/null +++ b/templates/compare_graph/compare_graphs.html @@ -0,0 +1,174 @@ +{% extends 'base.html' %} +{% block content %} + {% load staticfiles %} + + +
    +
    +
    + + +
    + + +
    + +
    +

    + Graph Comparison Page +

    +
    +
    +
    + + + + + +
    + +
    + + + + +
    + +
    + + + + +
    +
    + +
    +
    + + +
    + + + +
    + +
    + {% include 'compare_graph/nodes_table.html' %} +
    + +
    + {% include 'compare_graph/edges_table.html' %} +
    + +
    + {% include 'compare_graph/compare_visualization.html' %} +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/compare_graph/compare_visualization.html b/templates/compare_graph/compare_visualization.html new file mode 100644 index 00000000..02bed206 --- /dev/null +++ b/templates/compare_graph/compare_visualization.html @@ -0,0 +1,7 @@ +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/templates/compare_graph/edges_table.html b/templates/compare_graph/edges_table.html new file mode 100644 index 00000000..46f98a8e --- /dev/null +++ b/templates/compare_graph/edges_table.html @@ -0,0 +1,13 @@ +
    + + + + {# #} + {# #} + + + + + +

    Primary Graph

    Secondary Graph

    +
    diff --git a/templates/compare_graph/nodes_table.html b/templates/compare_graph/nodes_table.html new file mode 100644 index 00000000..134b0df5 --- /dev/null +++ b/templates/compare_graph/nodes_table.html @@ -0,0 +1,13 @@ +
    + + + + {# #} + {# #} + + + + + +

    Primary Graph

    Secondary Graph

    +
    diff --git a/templates/graph/default_sidebar.html b/templates/graph/default_sidebar.html index 9214c445..30145fb5 100644 --- a/templates/graph/default_sidebar.html +++ b/templates/graph/default_sidebar.html @@ -34,7 +34,13 @@ Change Layout + {% if uid %} +
  • + + Save Layout + +
  • Use Layout
    Editor diff --git a/templates/graph/graph_details_tab.html b/templates/graph/graph_details_tab.html index de38caa3..ee24f498 100644 --- a/templates/graph/graph_details_tab.html +++ b/templates/graph/graph_details_tab.html @@ -28,6 +28,23 @@ {% endfor %} + + + + + + {% if shared_groups %} + {% for k in shared_groups %} + + + + {% endfor %} + {% else %} + + + + {% endif %} +
    Shared with groups
    {{k.name|safe}}
    'The graph is not shared with any groups'
  • diff --git a/templates/graph/index.html b/templates/graph/index.html index 830d44f3..7aceed39 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -116,7 +116,8 @@ +