diff --git a/api/cueSearch/services/cardTemplate.py b/api/cueSearch/services/cardTemplate.py index e3bd72b6..4ca16cde 100644 --- a/api/cueSearch/services/cardTemplate.py +++ b/api/cueSearch/services/cardTemplate.py @@ -1,9 +1,14 @@ +import json import logging from utils.apiResponse import ApiResponse from cueSearch.serializers import SearchCardTemplateSerializer from cueSearch.models import SearchCardTemplate from dataset.models import ConnectionType +from django.template import Template, Context +from cueSearch.services.searchCardTemplate import SearchCardTemplateServices +from cueSearch.services.sampleParams import SAMPLE_PARAMS +logger = logging.getLogger(__name__) class CardTemplates: """ @@ -16,7 +21,7 @@ def createCardTemplate(payload: dict): Create search card template """ try: - res = ApiResponse("Error occur while creating search card template") + res = ApiResponse("Error occurred while creating search card template") connectionTypeId = int(payload.get("connectionTypeId", 1)) connectionType = ConnectionType.objects.get(id=connectionTypeId) renderType = payload.get("renderType", "table") @@ -36,7 +41,7 @@ def createCardTemplate(payload: dict): res.update(True, "Search card template created successfully") except Exception as ex: logging.error("Error %s", str(ex)) - res.update(False, "Exception occured while creating templates") + res.update(False, "Exception occurred while creating templates") return res @staticmethod @@ -96,7 +101,7 @@ def publishedCardTemplate(payload: dict): res.update(True, "Card Template published successfully") except Exception as ex: logging.error("Error %s", str(ex)) - res.update(False, "Error occured while publishing Card Template") + res.update(False, "Error occurred while publishing Card Template") return res def deleteCardTemplate(templateId: int): @@ -108,7 +113,7 @@ def deleteCardTemplate(templateId: int): res.update(True, "Card template deleted successfully") except Exception as ex: logging.error("Error while deleting %s", str(ex)) - res.update(False, "Error occured while deleting card template") + res.update(False, "Error occurred while deleting card template") return res @staticmethod @@ -121,5 +126,23 @@ def getCardTemplateById(templateId: int): res.update(True, "Fetched card templates", data) except Exception as ex: logging.error("Error while get card template by Id %s", str(ex)) - res.update(False, "Error occured while getting template by id") + res.update(False, "Error occurred while getting template by id") return res + + @staticmethod + def verifyCardTemplate(payload: dict): + res = ApiResponse() + try: + sampleParams = json.loads(json.dumps(SAMPLE_PARAMS)) + param = { + **sampleParams, + "templateTitle": payload['templateTitle'], + "templateText": payload['templateText'], + "templateSql": payload['templateSql'], + } + response = SearchCardTemplateServices.renderTemplatesUnsafe(param) + res.update(True,"Template rendered successfully") + except Exception as ex: + logger.error("Error in rendering templates: %s", str(ex)) + res.update(False,"Error occurred during rendering", str(ex)) + return res \ No newline at end of file diff --git a/api/cueSearch/services/sampleParams.py b/api/cueSearch/services/sampleParams.py new file mode 100644 index 00000000..abfe2aec --- /dev/null +++ b/api/cueSearch/services/sampleParams.py @@ -0,0 +1,111 @@ +SAMPLE_PARAMS ={ + 'dataset': 'Orders', + 'datasetId': 2, + 'datasetSql': "SELECT DATE_TRUNC('DAY', __time) as OrderDate,\n"'Brand, Color, State,\n''SUM("count") as Orders, ROUND(sum(OrderAmount),2) as ''OrderAmount, sum(OrderQuantity) as OrderQuantity\n''FROM FAKEORDERS\n'"WHERE __time >= CURRENT_TIMESTAMP - INTERVAL '13' MONTH \n"'GROUP BY 1, 2, 3, 4\n''ORDER BY 1', + 'dimensions': [ + 'Brand', + 'Color', + 'State' + ], + 'filter': "( ( Brand = 'Adidas' OR Brand = 'Nike' ) ) AND ( State = 'MS' OR ""State = 'KS' )", + 'filterDimensions': [ + 'Brand', + 'State' + ], + 'granularity': 'day', + 'groupedResultsForFilter': [ + [ + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'Brand', + 'globalDimensionName': 'Brand', + 'id': 2, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Brand', + 'value': 'Adidas' + }, + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'Brand', + 'globalDimensionName': 'Brand', + 'id': 2, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Brand', + 'value': 'Nike' + } + ], + [ + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'State', + 'globalDimensionName': 'Region', + 'id': 1, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Region', + 'value': 'MS' + }, + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'State', + 'globalDimensionName': 'Region', + 'id': 1, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Region', + 'value': 'KS' + } + ] + ], + 'metrics': [ + 'Orders', + 'OrderAmount', + 'OrderQuantity' + ], + 'renderType': 'line', + 'searchResults': [ + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'State', + 'globalDimensionName': 'Region', + 'id': 1, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Region', + 'value': 'MS' + }, + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'State', + 'globalDimensionName': 'Region', + 'id': 1, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Region', + 'value': 'KS' + }, + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'Brand', + 'globalDimensionName': 'Brand', + 'id': 2, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Brand', + 'value': 'Adidas' + }, + { + 'dataset': 'Orders', + 'datasetId': 2, + 'dimension': 'Brand', + 'globalDimensionName': 'Brand', + 'id': 2, + 'type': 'GLOBALDIMENSION', + 'user_entity_identifier': 'Brand', + 'value': 'Nike' + } + ], + 'timestampColumn': 'OrderDate' +} diff --git a/api/cueSearch/services/searchCardTemplate.py b/api/cueSearch/services/searchCardTemplate.py index 92bd4de4..71fd3f7f 100644 --- a/api/cueSearch/services/searchCardTemplate.py +++ b/api/cueSearch/services/searchCardTemplate.py @@ -191,35 +191,46 @@ def renderTemplates(param: dict): returns: [{ title: str, text: str, sql: str }] """ response = [] - delimiter = "+-;" try: - titles = ( - Template(param["templateTitle"]).render(Context(param)).split(delimiter) - ) - texts = ( - Template(param["templateText"]).render(Context(param)).split(delimiter) - ) - sqls = ( - Template(param["templateSql"]).render(Context(param)).split(delimiter) - ) - - if len(titles) != len(texts) or len(titles) != len(sqls): - raise ValueError( - "Inconsistent use of delimiter (%s) in title, text, sql of template" - % delimiter - ) - - for i in range(len(sqls)): - if str.isspace(sqls[i]): - continue - response.append({"title": titles[i], "text": texts[i], "sql": sqls[i]}) - + response = SearchCardTemplateServices.renderTemplatesUnsafe(param) except Exception as ex: logger.error("Error in rendering templates: %s", str(ex)) logger.error(param) return response + @staticmethod + def renderTemplatesUnsafe(param: dict): + """ + Renders template with passed variables, without error handling + :param param: dict with values needed for rendering + returns: [{ title: str, text: str, sql: str }] + """ + response = [] + delimiter = "+-;" + titles = ( + Template(param["templateTitle"]).render(Context(param)).split(delimiter) + ) + texts = ( + Template(param["templateText"]).render(Context(param)).split(delimiter) + ) + sqls = ( + Template(param["templateSql"]).render(Context(param)).split(delimiter) + ) + if len(titles) != len(texts) or len(titles) != len(sqls): + raise ValueError( + "Inconsistent use of delimiter (%s) in title, text, sql of template" + % delimiter + ) + + for i in range(len(sqls)): + if str.isspace(sqls[i]): + continue + response.append({"title": titles[i], "text": texts[i], "sql": sqls[i]}) + + return response + + @staticmethod def getSearchSuggestions(query): """Get searchsuggestion for search dropdown""" diff --git a/api/cueSearch/tests/test_cardTemplate.py b/api/cueSearch/tests/test_cardTemplate.py index 83dfd8a6..4d89e26a 100644 --- a/api/cueSearch/tests/test_cardTemplate.py +++ b/api/cueSearch/tests/test_cardTemplate.py @@ -1,4 +1,5 @@ import pytest +from unittest import mock from django.urls import reverse from mixer.backend.django import mixer @@ -19,14 +20,14 @@ def testCardTemplates(client, mocker): "connectionTypeId": 1, } response = client.post(path, payload, content_type="application/json") - assert response.data["success"] assert response.status_code == 200 + assert response.data["success"] # Getting the existing templates path = reverse("getTemplates") response = client.get(path, content_type="application/json") - assert response.data["success"] assert response.status_code == 200 + assert response.data["success"] response.json()["data"][0]["templateName"] == "Sample" response.json()["data"][0]["published"] == False id = response.json()["data"][0]["id"] @@ -35,14 +36,14 @@ def testCardTemplates(client, mocker): payload = {"id": id, "published": False} path = reverse("pubCardTemplates") response = client.post(path, payload, content_type="application/json") - assert response.data["success"] assert response.status_code == 200 + assert response.data["success"] # Getting the template by Id path = reverse("getTemplatesById", kwargs={"id": id}) response = client.get(path, content_type="application/json") - assert response.data["success"] assert response.status_code == 200 + assert response.data["success"] # Updating the card template payload = { @@ -59,13 +60,69 @@ def testCardTemplates(client, mocker): # Getting the existing templates path = reverse("getTemplates") response = client.get(path, content_type="application/json") - assert response.data["success"] assert response.status_code == 200 + assert response.data["success"] response.json()["data"][0]["templateName"] == "updatedSample" response.json()["data"][0]["title"] == "updatedSample" # Deleting the existing templates path = reverse("cardTemplateDelete", kwargs={"id": id}) response = client.delete(path, payload, content_type="application/json") + assert response.status_code == 200 assert response.data["success"] + + + + +@pytest.mark.django_db(transaction=True) +def testVerifyCardTemplates(client, mocker): + + templateTitle = "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} Comparison of {{metricName}} among {{filterDim}} values in {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}" + templateText = "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} This chart displays filtered values on dimension {{filterDim}} along with other filters applied i.e. {{filter|safe}} for metric {{metricName}} on dataset {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}" + templateSql = "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} SELECT \"templatetable\".\"{{ timestampColumn }}\", \"templatetable\".\"{{ filterDim }}\", SUM(\"templatetable\".\"{{ metricName }}\") as {{metricName}} FROM ({{ datasetSql|safe }}) AS templatetable WHERE {% for orResults in groupedResultsForFilter %} {% for orResult in orResults %} \"templatetable\".\"{{ orResult.dimension }}\" = '{{ orResult.value }}' OR {% endfor %} True AND {% endfor %} True GROUP BY 1, 2 limit 500 +-; {% endfor %} {% endif %} {% endfor %}" + noVariableTemplateSql = "{% load event_tags %} {% for filterDim in filterDimensionx %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} SELECT \"templatetable\".\"{{ timestampColumn }}\", \"templatetable\".\"{{ filterDim }}\", SUM(\"templatetable\".\"{{ metricName }}\") as {{metricName}} FROM ({{ datasetSql|safe }}) AS templatetable WHERE {% for orResults in groupedResultsForFilter %} {% for orResult in orResults %} \"templatetable\".\"{{ orResult.dimension }}\" = '{{ orResult.value }}' OR {% endfor %} True AND {% endfor %} True GROUP BY 1, 2 limit 500 +-; {% endfor %} {% endif %} {% endfor %}" + + # Testing the card templete api + path = reverse("verifyCardTemplates") + payload = { + "templateTitle": templateTitle, + "templateText": templateText, + "templateSql": templateSql, + } + response = client.post(path, payload, content_type="application/json") + assert response.status_code == 200 + assert response.data["success"] == True + + + path = reverse("verifyCardTemplates") + payload = { + "templateTitle": templateTitle, + "templateText": templateText, + "templateSql": noVariableTemplateSql, + } + response = client.post(path, payload, content_type="application/json") + assert response.status_code == 200 + assert response.data["success"] == False + + # different number of values returning from templateText vs templateSql + path = reverse("verifyCardTemplates") + payload = { + "templateTitle": templateTitle, + "templateText": "", + "templateSql": noVariableTemplateSql, + } + response = client.post(path, payload, content_type="application/json") assert response.status_code == 200 + assert response.data["success"] == False + + # empty values for templateText and templateSql resulting to valid template + path = reverse("verifyCardTemplates") + payload = { + "templateTitle": "", + "templateText": "", + "templateSql": "", + } + response = client.post(path, payload, content_type="application/json") + assert response.status_code == 200 + assert response.data["success"] == True + diff --git a/api/cueSearch/urls.py b/api/cueSearch/urls.py index ee81a23f..f7843550 100644 --- a/api/cueSearch/urls.py +++ b/api/cueSearch/urls.py @@ -59,4 +59,5 @@ name="cardTemplateDelete", ), path("templates/publish/", views.pubCardTemplate, name="pubCardTemplates"), + path("templates/verify/", views.verifyCardTemplate, name="verifyCardTemplates"), ] diff --git a/api/cueSearch/views.py b/api/cueSearch/views.py index ee2773f8..40ec12a7 100644 --- a/api/cueSearch/views.py +++ b/api/cueSearch/views.py @@ -157,3 +157,11 @@ def pubCardTemplate(request: HttpRequest) -> Response: payload = request.data res = CardTemplates.publishedCardTemplate(payload) return Response(res.json()) + +@api_view(['POST']) +def verifyCardTemplate(request: HttpRequest) -> Response: + """Method to verify sql""" + payload = request.data + res = CardTemplates.verifyCardTemplate(payload) + return Response(res.json()) + diff --git a/api/seeddata/searchCardTemplate.json b/api/seeddata/searchCardTemplate.json index fe3b441e..9fae1cf4 100644 --- a/api/seeddata/searchCardTemplate.json +++ b/api/seeddata/searchCardTemplate.json @@ -3,7 +3,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 1, "fields": { - "published": false, + "published": true, "templateName": "Table of all data", "title": "Dataset = {{dataset}} where {{filter}}", "bodyText": "This table displays raw data for dataset {{dataset}} with filter {{filter}} ", @@ -16,7 +16,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 2, "fields": { - "published": false, + "published": true, "templateName": "Split on Filter Dimension", "title": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} Comparison of {{metricName}} among {{filterDim}} values in {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", "bodyText": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} This chart displays filtered values on dimension {{filterDim}} along with other filters applied i.e. {{filter|safe}} for metric {{metricName}} on dataset {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", @@ -29,7 +29,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 3, "fields": { - "published": false, + "published": true, "templateName": "Metric Chart", "title": " {% for metric in metrics %} {{ metric }} from {{dataset}} where {{filter}} +-; {% endfor %}", "bodyText": " {% for metric in metrics %} For {{filter}} +-; {% endfor %}", @@ -42,7 +42,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 4, "fields": { - "published": false, + "published": true, "templateName": "Table of all data", "title": "Dataset = {{dataset}} where {{filter}}", "bodyText": "This table displays raw data for dataset {{dataset}} with filter {{filter}} ", @@ -55,7 +55,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 5, "fields": { - "published": false, + "published": true, "templateName": "Split on Filter Dimension", "title": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} Comparison of {{metricName}} among {{filterDim}} values in {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", "bodyText": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} This chart displays filtered values on dimension {{filterDim}} along with other filters applied i.e. {{filter|safe}} for metric {{metricName}} on dataset {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", @@ -68,7 +68,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 6, "fields": { - "published": false, + "published": true, "templateName": "Metric Chart", "title": " {% for metric in metrics %} {{ metric }} from {{dataset}} where {{filter}} +-; {% endfor %}", "bodyText": " {% for metric in metrics %} For {{filter}} +-; {% endfor %}", @@ -81,7 +81,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 7, "fields": { - "published": false, + "published": true, "templateName": "Table of all data", "title": "Dataset = {{dataset}} where {{filter}}", "bodyText": "This table displays raw data for dataset {{dataset}} with filter {{filter}} ", @@ -94,7 +94,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 8, "fields": { - "published": false, + "published": true, "templateName": "Split on Filter Dimension", "title": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} Comparison of {{metricName}} among {{filterDim}} values in {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", "bodyText": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} This chart displays filtered values on dimension {{filterDim}} along with other filters applied i.e. {{filter|safe}} for metric {{metricName}} on dataset {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", @@ -107,7 +107,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 9, "fields": { - "published": false, + "published": true, "templateName": "Metric Chart", "title": " {% for metric in metrics %} {{ metric }} from {{dataset}} where {{filter}} +-; {% endfor %}", "bodyText": " {% for metric in metrics %} For {{filter}} +-; {% endfor %}", @@ -120,7 +120,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 10, "fields": { - "published": false, + "published": true, "templateName": "Table of all data", "title": "Dataset = {{dataset}} where {{filter}}", "bodyText": "This table displays raw data for dataset {{dataset}} with filter {{filter}} ", @@ -133,7 +133,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 11, "fields": { - "published": false, + "published": true, "templateName": "Split on Filter Dimension", "title": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} Comparison of {{metricName}} among {{filterDim}} values in {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", "bodyText": "{% load event_tags %} {% for filterDim in filterDimensions %} {% conditionalCount searchResults 'dimension' filterDim as dimCount %} {% if dimCount > 1 %} {% for metricName in metrics %} This chart displays filtered values on dimension {{filterDim}} along with other filters applied i.e. {{filter|safe}} for metric {{metricName}} on dataset {{dataset}} +-; {% endfor %} {% endif %} {% endfor %}", @@ -146,7 +146,7 @@ "model": "cueSearch.searchCardTemplate", "pk": 12, "fields": { - "published": false, + "published": true, "templateName": "Metric Chart", "title": " {% for metric in metrics %} {{ metric }} from {{dataset}} where {{filter}} +-; {% endfor %}", "bodyText": " {% for metric in metrics %} For {{filter}} +-; {% endfor %}", diff --git a/test.py b/test.py new file mode 100644 index 00000000..64f91118 --- /dev/null +++ b/test.py @@ -0,0 +1,4 @@ +param = { + "id":1, + "color":"black" +} \ No newline at end of file diff --git a/ui/src/components/Search/CardTemplates/AddCardTemplates.js b/ui/src/components/Search/CardTemplates/AddCardTemplates.js index 3e29a748..7c9f7ad3 100644 --- a/ui/src/components/Search/CardTemplates/AddCardTemplates.js +++ b/ui/src/components/Search/CardTemplates/AddCardTemplates.js @@ -5,6 +5,7 @@ import style from "./style.module.scss"; import cardTemplateService from "services/main/cardTemplate"; import connectionService from "services/main/connection.js"; +import { template } from "lodash"; const { TextArea } = Input; @@ -39,7 +40,20 @@ export default function AddCardTemplates(props) { payload["bodyText"] = values["bodyText"]; payload["renderType"] = values["renderType"]; + let verifyPayload ={}; + verifyPayload = { + "templateTitle": payload['title'], + "templateText" : payload['bodyText'], + "templateSql":payload['sql'], + } + const response = await cardTemplateService.verifyCardTemplate(verifyPayload); + + if (response.success){ const response = await cardTemplateService.addCardTemplate(payload); + } else { + message.error(response.message); + } + if (response.success) { props.onAddCardTemplateSuccess(); } else { diff --git a/ui/src/services/main/cardTemplate.js b/ui/src/services/main/cardTemplate.js index 3231ab98..f47f2878 100644 --- a/ui/src/services/main/cardTemplate.js +++ b/ui/src/services/main/cardTemplate.js @@ -20,6 +20,16 @@ class CardTemplateServices { return response; } } + + async verifyCardTemplate(payload) { + const response = await apiService.post( + "cueSearch/templates/verify/", + payload + ); + return response; + } + + async updateCardTemplate(id, payload) { const response = await apiService.post( "cueSearch/templates/update/" + id,