From 9ddabbc36c4c73f3b93e7e963bdf7759a9ff129f Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Tue, 15 Jul 2025 08:40:54 -0700 Subject: [PATCH 01/16] Add Chart.js and some initial GraphQL configuration --- forms-flow-web/package-lock.json | 49 +++++ forms-flow-web/package.json | 2 + .../src/apiManager/endpoints/config.js | 6 +- .../src/apiManager/endpoints/index.js | 2 + .../apiManager/services/metricsServices.js | 35 ++- .../src/components/Dashboard/Dashboard.js | 24 +- .../components/Dashboard/StatusChart-POC.js | 208 ++++++++++++++++++ .../src/components/Dashboard/StatusChart.js | 22 +- 8 files changed, 332 insertions(+), 16 deletions(-) create mode 100644 forms-flow-web/src/components/Dashboard/StatusChart-POC.js diff --git a/forms-flow-web/package-lock.json b/forms-flow-web/package-lock.json index f00a5bd3eb..e0dab0f600 100644 --- a/forms-flow-web/package-lock.json +++ b/forms-flow-web/package-lock.json @@ -33,6 +33,7 @@ "camunda-bpmn-js-behaviors": "^1.2.1", "camunda-bpmn-moddle": "^7.0.1", "camunda-dmn-moddle": "^1.3.0", + "chart.js": "^4.5.0", "connected-react-router": "^6.9.1", "craco-plugin-single-spa-app-aot": "^2.0.4", "create-react-class": "^15.7.0", @@ -53,6 +54,7 @@ "querystring": "^0.2.1", "react": "^17.0.2", "react-bootstrap": "^1.6.0", + "react-chartjs-2": "^5.3.0", "react-date-range": "^1.1.3", "react-datepicker": "^3.8.0", "react-dom": "^17.0.2", @@ -3469,6 +3471,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -7234,6 +7242,18 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -17027,6 +17047,16 @@ "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-date-picker": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz", @@ -24173,6 +24203,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -26939,6 +26974,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -33956,6 +33999,12 @@ "prop-types": "^15.6.0" } }, + "react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "requires": {} + }, "react-date-picker": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz", diff --git a/forms-flow-web/package.json b/forms-flow-web/package.json index b2d9842c6a..0851682995 100644 --- a/forms-flow-web/package.json +++ b/forms-flow-web/package.json @@ -64,6 +64,7 @@ "camunda-bpmn-js-behaviors": "^1.2.1", "camunda-bpmn-moddle": "^7.0.1", "camunda-dmn-moddle": "^1.3.0", + "chart.js": "^4.5.0", "connected-react-router": "^6.9.1", "craco-plugin-single-spa-app-aot": "^2.0.4", "create-react-class": "^15.7.0", @@ -84,6 +85,7 @@ "querystring": "^0.2.1", "react": "^17.0.2", "react-bootstrap": "^1.6.0", + "react-chartjs-2": "^5.3.0", "react-date-range": "^1.1.3", "react-datepicker": "^3.8.0", "react-dom": "^17.0.2", diff --git a/forms-flow-web/src/apiManager/endpoints/config.js b/forms-flow-web/src/apiManager/endpoints/config.js index 9e95cf2b82..48cd83d44c 100644 --- a/forms-flow-web/src/apiManager/endpoints/config.js +++ b/forms-flow-web/src/apiManager/endpoints/config.js @@ -2,7 +2,11 @@ export const WEB_BASE_URL = (window._env_ && window._env_.REACT_APP_WEB_BASE_URL) || process.env.REACT_APP_WEB_BASE_URL; - export const DOCUMENT_SERVICE_URL = +export const GRAPHQL_URL = + (window._env_ && window._env_.REACT_APP_GRAPHQL_API_URL) || + process.env.REACT_APP_GRAPHQL_API_URL; + +export const DOCUMENT_SERVICE_URL = (window._env_ && window._env_.REACT_APP_DOCUMENT_SERVICE_URL) || process.env.REACT_APP_DOCUMENT_SERVICE_URL; diff --git a/forms-flow-web/src/apiManager/endpoints/index.js b/forms-flow-web/src/apiManager/endpoints/index.js index 5e8a44e1e1..8ead3525c8 100644 --- a/forms-flow-web/src/apiManager/endpoints/index.js +++ b/forms-flow-web/src/apiManager/endpoints/index.js @@ -1,6 +1,7 @@ /* eslint-disable max-len */ import { WEB_BASE_URL, + GRAPHQL_URL, MT_ADMIN_BASE_URL, MT_ADMIN_BASE_URL_VERSION, BPM_BASE_URL_EXT, @@ -11,6 +12,7 @@ import { import { AppConfig } from "../../config"; const API = { + GRAPHQL: GRAPHQL_URL, GET_DASHBOARDS: `${WEB_BASE_URL}/dashboards`, METRICS_SUBMISSIONS: `${WEB_BASE_URL}/metrics`, APPLICATION_START: `${WEB_BASE_URL}/application/create`, diff --git a/forms-flow-web/src/apiManager/services/metricsServices.js b/forms-flow-web/src/apiManager/services/metricsServices.js index 00774ba7e9..0e8df86ed1 100644 --- a/forms-flow-web/src/apiManager/services/metricsServices.js +++ b/forms-flow-web/src/apiManager/services/metricsServices.js @@ -27,20 +27,39 @@ export const fetchMetricsSubmissionCount = ( const done = rest.length ? rest[0] : () => { }; return (dispatch) => { dispatch(setMetricsLoadError(false)); + const url = API.GRAPHQL; /*eslint max-len: ["error", { "code": 170 }]*/ - let url = `${API.METRICS_SUBMISSIONS}?from=${fromDate}&to=${toDate}&orderBy=${searchBy}&pageNo=${pageNo}&limit=${limit}&sortBy=${sortsBy}&sortOrder=${sortOrder}`; - if (formName) { - url += `&formName=${encodeURIComponent(formName)}`; - } - - RequestService.httpGETRequest(url, {}) + const query = ` + query FetchMetricsSubmissionQuery { + getSubmission(sortBy: "${sortsBy}", sortOrder: "${sortOrder}", pageNo: ${pageNo}, limit: ${limit}, filters: { + from: "${fromDate}", + to: "${toDate}", + orderBy: "${searchBy}", + }) { + submissions { + applicationStatus + created + createdBy + data + formName + id + submissionId + } + totalCount + } + } + `; + + RequestService.httpPOSTRequest(url, { + query: query + }) .then((res) => { if (res.data) { dispatch(setMetricsDateRangeLoading(false)); dispatch(setMetricsLoader(false)); dispatch(setMetricsStatusLoader(false)); - dispatch(setMetricsSubmissionCount(res.data.applications)); - dispatch(setMetricsTotalItems(res.data.totalCount)); + dispatch(setMetricsSubmissionCount(res.data.data.getSubmission.submissions)); + dispatch(setMetricsTotalItems(res.data.data.getSubmission.totalCount)); if (res.data.applications && res.data.applications[0]) { dispatch(setSelectedMetricsId(res.data.applications[0].parentFormId)); diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index b02b34176f..1698e00818 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -4,6 +4,7 @@ import ApplicationCounter from "./ApplicationCounter"; import { useDispatch, useSelector } from "react-redux"; import { Route, Redirect } from "react-router"; import StatusChart from "./StatusChart"; +import StatusChart_POC from "./StatusChart-POC"; import Modal from "react-bootstrap/Modal"; import { fetchMetricsSubmissionCount, @@ -358,12 +359,23 @@ const Dashboard = React.memo(() => { - +
+

Redash

+ +
+

Charts.js

+ +
)} diff --git a/forms-flow-web/src/components/Dashboard/StatusChart-POC.js b/forms-flow-web/src/components/Dashboard/StatusChart-POC.js new file mode 100644 index 0000000000..aa148376a9 --- /dev/null +++ b/forms-flow-web/src/components/Dashboard/StatusChart-POC.js @@ -0,0 +1,208 @@ +import React, { useMemo, useState } from "react"; +import { FormControl } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; +import LoadingOverlay from "react-loading-overlay-ts"; + +import { + ArcElement, + BarElement, + CategoryScale, + Chart as ChartJS, + Filler, + Legend, + LinearScale, + LineElement, + PointElement, + RadialLinearScale, + Tooltip +} from 'chart.js'; +import { Bar, Doughnut, Pie, PolarArea, Radar } from 'react-chartjs-2'; + +ChartJS.register( + ArcElement, + BarElement, + CategoryScale, + Filler, + Legend, + LinearScale, + LineElement, + PointElement, + RadialLinearScale, + Tooltip +); + +const chartTypes = [ + { value: 'pie', label: 'Pie Chart' }, + { value: 'v-bar', label: 'Vertical Bar Chart' }, + { value: 'doughnut', label: 'Doughnut Chart' }, + { value: 'polar-area', label: 'Polar Area Chart' }, + { value: 'radar', label: 'Radar Chart' }, +]; + +const testData = { + labels: ["Completed", "In Progress", "Inactive", "Other"], + datasets: [ + { + label: 'Submissions Dataset', + data: [10, 4, 3, 1], + backgroundColor: [ + "#0088FE", + "#00C49F", + "#FFBB28", + "#FF8042" + ] + } + ] +}; + + +const ChartForm = React.memo((props) => { + const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; + const { formVersions, formName, parentFormId } = submissionData; + + const [selectedChartValue, setSelectedChartValue] = useState('pie'); + const sortedVersions = useMemo(() => + (formVersions?.sort((version1, version2) => + version1.version > version2.version ? 1 : -1)), [formVersions]); + + const version = formVersions?.length; + const { t } = useTranslation(); + const pieData = submissionsStatusList || []; + console.log(pieData); + // pieData = testData; + + const handlePieData = (value) => { + const isParentId = value === "all"; + const id = isParentId ? parentFormId : value; + const option = { parentId: isParentId }; + props.getStatusDetails(id, option); + }; + + const chartOptions = { + plugins: { + legend: { + position: 'bottom' + } + } + }; + + const renderChart = () => { + switch (selectedChartValue) { + case 'pie': + return ( + + ); + case 'v-bar': + return ( + + ); + case 'doughnut': + return ( + + ); + case 'polar-area': + return ( + + ); + case 'radar': + return ( + + ); + } + }; + + + return ( +
+
+
+
+
+
+ + {t("Form Name")} : + +

+ {formName} +

+
+ +

+ {t("Latest Version")} :{" "} + {`v${version}`} +

+
+ {sortedVersions.length > 1 && ( +
+

+ {t("Select form version")}: +

+ +
+ )} +
+ +
+
+ {renderChart()} +
+
+
+ setSelectedChartValue(e.target.value)} + className="form-select p-1" + title={t("Choose any")} + aria-label="Select chart type" + > + {chartTypes.map((option, index) => ( + + ))} + +
+
+
+
+
+
+
+ ); +}); + +export default ChartForm; \ No newline at end of file diff --git a/forms-flow-web/src/components/Dashboard/StatusChart.js b/forms-flow-web/src/components/Dashboard/StatusChart.js index fc4943e43b..994333db2e 100644 --- a/forms-flow-web/src/components/Dashboard/StatusChart.js +++ b/forms-flow-web/src/components/Dashboard/StatusChart.js @@ -15,6 +15,25 @@ const COLORS = [ "#ff7c43", ]; +const testData = [ + { + "statusName": "Completed", + "count": 10 + }, + { + "statusName": "In Progress", + "count": 4 + }, + { + "statusName": "Inactive", + "count": 3 + }, + { + "statusName": "Other", + "count": 1 + } +]; + // label={renderCustomizedLabel} const ChartForm = React.memo((props) => { const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; @@ -26,7 +45,8 @@ const ChartForm = React.memo((props) => { const version = formVersions?.length; const { t } = useTranslation(); - const pieData = submissionsStatusList || []; + let pieData = submissionsStatusList || []; + pieData = testData; const handlePieData = (value) => { const isParentId = value === "all"; From 476edc4e6e1b731d34d0f577e9b22273981cea10 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Sun, 20 Jul 2025 19:03:13 -0700 Subject: [PATCH 02/16] Add form name param to graphql submission query --- .../graphql/resolvers/submission_resolvers.py | 3 + .../src/graphql/service/submission_service.py | 5 +- .../src/models/webapi/application.py | 7 +- .../apiManager/services/metricsServices.js | 3 +- .../src/components/Dashboard/Dashboard.js | 12 - .../components/Dashboard/StatusChart-POC.js | 208 --------------- .../src/components/Dashboard/StatusChart.js | 251 +++++++++--------- 7 files changed, 143 insertions(+), 346 deletions(-) delete mode 100644 forms-flow-web/src/components/Dashboard/StatusChart-POC.js diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index de82637363..69cb9b15bf 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -14,6 +14,7 @@ class QuerySubmissionsResolver: async def get_submission( self, info: strawberry.Info, + form_name: Optional[str] = None, sort_by: str = "created", sort_order: str = "desc", parent_form_id: Optional[str] = None, @@ -29,6 +30,7 @@ async def get_submission( Args: info (strawberry.Info): GraphQL context information + form_name (Optional[str]): Name of the form sort_by (str): Field to sort by (default: "created") sort_order (str): Order of sorting (default: "desc") parent_form_id (Optional[str]): ID of the parent form @@ -43,6 +45,7 @@ async def get_submission( """ submission = await SubmissionService.get_submission( info=info, + form_name=form_name, sort_by=sort_by, sort_order=sort_order, parent_form_id=parent_form_id, diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index 780e8a0e08..00c680559f 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -83,6 +83,7 @@ def _process_results( @staticmethod async def get_submission( info: strawberry.Info, + form_name: str, sort_by: str, sort_order: str, parent_form_id: str, @@ -95,7 +96,8 @@ async def get_submission( ) -> Optional[PaginatedSubmissionResponse]: """ Fetches submissions from both webapi and MongoDB, merges them, and returns a paginated response. - Args:sort_by: Field to sort by (default: "created") + Args: form_name: Name of the form + sort_by: Field to sort by (default: "created") sort_order: Order of sorting (default: "desc") parent_form_id: ID of the parent form filters: Filters to apply to the query @@ -133,6 +135,7 @@ async def get_submission( tenant_key=tenant_key, roles=user_groups, is_paginate=is_paginate_on_webapi_side, + form_name=form_name, filter=webapi_search, created_before=created_before, created_after=created_after, diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 30db01b92c..d25252bb8b 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -59,6 +59,7 @@ async def get_authorized_applications( cls, tenant_key: str, roles: list[str], + form_name: str, parent_form_id: str, filter: dict = None, created_before: str = None, @@ -93,7 +94,10 @@ async def get_authorized_applications( authorization_table=authorization_table, roles=roles ) - # Optional condition + # Optional conditions + form_name_condition = ( + mapper_table.c.form_name == form_name if form_name else True + ) parent_form_id_condition = ( mapper_table.c.parent_form_id == parent_form_id if parent_form_id else True ) @@ -103,6 +107,7 @@ async def get_authorized_applications( mapper_table, and_( application_table.c.form_process_mapper_id == mapper_table.c.id, + form_name_condition, # Ensure form name matches if provided parent_form_id_condition, # Ensure parent form ID matches if provided mapper_table.c.tenant == tenant_key, # Ensure tenant key matches in both tables diff --git a/forms-flow-web/src/apiManager/services/metricsServices.js b/forms-flow-web/src/apiManager/services/metricsServices.js index 0e8df86ed1..1df069cb57 100644 --- a/forms-flow-web/src/apiManager/services/metricsServices.js +++ b/forms-flow-web/src/apiManager/services/metricsServices.js @@ -27,11 +27,12 @@ export const fetchMetricsSubmissionCount = ( const done = rest.length ? rest[0] : () => { }; return (dispatch) => { dispatch(setMetricsLoadError(false)); + print('formName', formName); const url = API.GRAPHQL; /*eslint max-len: ["error", { "code": 170 }]*/ const query = ` query FetchMetricsSubmissionQuery { - getSubmission(sortBy: "${sortsBy}", sortOrder: "${sortOrder}", pageNo: ${pageNo}, limit: ${limit}, filters: { + getSubmission(formName: "${formName}", sortBy: "${sortsBy}", sortOrder: "${sortOrder}", pageNo: ${pageNo}, limit: ${limit}, filters: { from: "${fromDate}", to: "${toDate}", orderBy: "${searchBy}", diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index 1698e00818..2c938e3d72 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -4,7 +4,6 @@ import ApplicationCounter from "./ApplicationCounter"; import { useDispatch, useSelector } from "react-redux"; import { Route, Redirect } from "react-router"; import StatusChart from "./StatusChart"; -import StatusChart_POC from "./StatusChart-POC"; import Modal from "react-bootstrap/Modal"; import { fetchMetricsSubmissionCount, @@ -359,23 +358,12 @@ const Dashboard = React.memo(() => { -
-

Redash

-
-

Charts.js

- -
)} diff --git a/forms-flow-web/src/components/Dashboard/StatusChart-POC.js b/forms-flow-web/src/components/Dashboard/StatusChart-POC.js deleted file mode 100644 index aa148376a9..0000000000 --- a/forms-flow-web/src/components/Dashboard/StatusChart-POC.js +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useMemo, useState } from "react"; -import { FormControl } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import LoadingOverlay from "react-loading-overlay-ts"; - -import { - ArcElement, - BarElement, - CategoryScale, - Chart as ChartJS, - Filler, - Legend, - LinearScale, - LineElement, - PointElement, - RadialLinearScale, - Tooltip -} from 'chart.js'; -import { Bar, Doughnut, Pie, PolarArea, Radar } from 'react-chartjs-2'; - -ChartJS.register( - ArcElement, - BarElement, - CategoryScale, - Filler, - Legend, - LinearScale, - LineElement, - PointElement, - RadialLinearScale, - Tooltip -); - -const chartTypes = [ - { value: 'pie', label: 'Pie Chart' }, - { value: 'v-bar', label: 'Vertical Bar Chart' }, - { value: 'doughnut', label: 'Doughnut Chart' }, - { value: 'polar-area', label: 'Polar Area Chart' }, - { value: 'radar', label: 'Radar Chart' }, -]; - -const testData = { - labels: ["Completed", "In Progress", "Inactive", "Other"], - datasets: [ - { - label: 'Submissions Dataset', - data: [10, 4, 3, 1], - backgroundColor: [ - "#0088FE", - "#00C49F", - "#FFBB28", - "#FF8042" - ] - } - ] -}; - - -const ChartForm = React.memo((props) => { - const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; - const { formVersions, formName, parentFormId } = submissionData; - - const [selectedChartValue, setSelectedChartValue] = useState('pie'); - const sortedVersions = useMemo(() => - (formVersions?.sort((version1, version2) => - version1.version > version2.version ? 1 : -1)), [formVersions]); - - const version = formVersions?.length; - const { t } = useTranslation(); - const pieData = submissionsStatusList || []; - console.log(pieData); - // pieData = testData; - - const handlePieData = (value) => { - const isParentId = value === "all"; - const id = isParentId ? parentFormId : value; - const option = { parentId: isParentId }; - props.getStatusDetails(id, option); - }; - - const chartOptions = { - plugins: { - legend: { - position: 'bottom' - } - } - }; - - const renderChart = () => { - switch (selectedChartValue) { - case 'pie': - return ( - - ); - case 'v-bar': - return ( - - ); - case 'doughnut': - return ( - - ); - case 'polar-area': - return ( - - ); - case 'radar': - return ( - - ); - } - }; - - - return ( -
-
-
-
-
-
- - {t("Form Name")} : - -

- {formName} -

-
- -

- {t("Latest Version")} :{" "} - {`v${version}`} -

-
- {sortedVersions.length > 1 && ( -
-

- {t("Select form version")}: -

- -
- )} -
- -
-
- {renderChart()} -
-
-
- setSelectedChartValue(e.target.value)} - className="form-select p-1" - title={t("Choose any")} - aria-label="Select chart type" - > - {chartTypes.map((option, index) => ( - - ))} - -
-
-
-
-
-
-
- ); -}); - -export default ChartForm; \ No newline at end of file diff --git a/forms-flow-web/src/components/Dashboard/StatusChart.js b/forms-flow-web/src/components/Dashboard/StatusChart.js index 994333db2e..627f8ab890 100644 --- a/forms-flow-web/src/components/Dashboard/StatusChart.js +++ b/forms-flow-web/src/components/Dashboard/StatusChart.js @@ -1,58 +1,120 @@ -import React, { useMemo } from "react"; +import React, { useState } from "react"; +import { FormControl } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import LoadingOverlay from "react-loading-overlay-ts"; -import { Legend, PieChart, Pie, Cell } from "recharts"; +import { + ArcElement, + BarElement, + CategoryScale, + Chart as ChartJS, + Filler, + Legend, + LinearScale, + LineElement, + PointElement, + RadialLinearScale, + Tooltip +} from 'chart.js'; +import { Bar, Doughnut, Pie, PolarArea, Radar } from 'react-chartjs-2'; -const COLORS = [ - "#0088FE", - "#00C49F", - "#FFBB28", - "#FF8042", - "#a05195", - "#d45087", - "#f95d6a", - "#ff7c43", -]; +ChartJS.register( + ArcElement, + BarElement, + CategoryScale, + Filler, + Legend, + LinearScale, + LineElement, + PointElement, + RadialLinearScale, + Tooltip +); -const testData = [ - { - "statusName": "Completed", - "count": 10 - }, - { - "statusName": "In Progress", - "count": 4 - }, - { - "statusName": "Inactive", - "count": 3 - }, - { - "statusName": "Other", - "count": 1 - } +const chartTypes = [ + { value: 'pie', label: 'Pie Chart' }, + { value: 'v-bar', label: 'Vertical Bar Chart' }, + { value: 'doughnut', label: 'Doughnut Chart' }, + { value: 'polar-area', label: 'Polar Area Chart' }, + { value: 'radar', label: 'Radar Chart' }, ]; -// label={renderCustomizedLabel} +const testData = { + datasets: [ + { + label: 'Submissions Dataset', + data: [{"Completed": 10}, {"In Progress": 4}, {"Inactive": 3}, {"Other": 1}], + backgroundColor: [ + "#0088FE", + "#00C49F", + "#FFBB28", + "#FF8042", + "#a05195", + "#d45087", + "#f95d6a", + "#ff7c43" + ] + } + ] +}; + + const ChartForm = React.memo((props) => { const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; - const {formVersions, formName, parentFormId} = submissionData; + const { formName } = submissionData; - const sortedVersions = useMemo(()=> - (formVersions?.sort((version1, version2)=> - version1.version > version2.version ? 1 : -1)),[formVersions]); + const [selectedChartValue, setSelectedChartValue] = useState('pie'); - const version = formVersions?.length; const { t } = useTranslation(); - let pieData = submissionsStatusList || []; - pieData = testData; + const chartData = submissionsStatusList || []; + console.log(chartData); - const handlePieData = (value) => { - const isParentId = value === "all"; - const id = isParentId ? parentFormId : value; - const option = {parentId : isParentId}; - props.getStatusDetails(id,option); + const chartOptions = { + plugins: { + legend: { + position: 'bottom' + } + } + }; + + const renderChart = () => { + switch (selectedChartValue) { + case 'pie': + return ( + + ); + case 'v-bar': + return ( + + ); + case 'doughnut': + return ( + + ); + case 'polar-area': + return ( + + ); + case 'radar': + return ( + + ); + } }; @@ -72,90 +134,33 @@ const ChartForm = React.memo((props) => { {formName} - -

- {t("Latest Version")} :{" "} - {`v${version}`} -

- - {sortedVersions.length > 1 && ( -
-

- {t("Select form version")}: -

- -
-)} - - + -
- -
- - - - - {pieData.map((entry) => ( - -))} - - -
- { - pieData.length ? ( -
- {pieData.map((entry, index) => ( -
- -
{entry.statusName}
+ active={submissionStatusCountLoader} + spinner + text={t("Loading...")} + > +
+
+ {renderChart()} +
+
+
+ setSelectedChartValue(e.target.value)} + className="form-select p-1" + title={t("Choose any")} + aria-label="Select chart type" + > + {chartTypes.map((option, index) => ( + + ))} +
- ))} +
- ) : ( -
- {t("No submissions")} -
- )} -
-
@@ -163,4 +168,4 @@ const ChartForm = React.memo((props) => { ); }); -export default ChartForm; +export default ChartForm; \ No newline at end of file From 3ee59344eb4fdeca879c8be16cd78617886f288c Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Sun, 20 Jul 2025 19:59:03 -0700 Subject: [PATCH 03/16] Fix typo --- forms-flow-web/src/components/Dashboard/ApplicationCounter.js | 2 +- forms-flow-web/src/components/Dashboard/CardFormCounter.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js index 47aed94df4..de01fcd2ef 100644 --- a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js +++ b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js @@ -33,7 +33,7 @@ const ApplicationCounter = React.memo((props) => { key={idx} > diff --git a/forms-flow-web/src/components/Dashboard/CardFormCounter.js b/forms-flow-web/src/components/Dashboard/CardFormCounter.js index 0c49e4c5d0..958ee4e5cc 100644 --- a/forms-flow-web/src/components/Dashboard/CardFormCounter.js +++ b/forms-flow-web/src/components/Dashboard/CardFormCounter.js @@ -4,11 +4,11 @@ import { Translation } from "react-i18next"; import { useSelector } from "react-redux"; const CardFormCounter = React.memo((props) => { - const { submitionData, getStatusDetails } = props; + const { submissionData, getStatusDetails } = props; const selectedMetricsId = useSelector( (state) => state.metrics?.selectedMetricsId ); - const { formName, parentFormId, applicationCount } = submitionData; + const { formName, parentFormId, applicationCount } = submissionData; return (
Date: Mon, 21 Jul 2025 21:24:12 -0700 Subject: [PATCH 04/16] Update GraphQl forms queries to pull from multple db sources --- forms-flow-data-layer/src/db/formio_db.py | 4 +- .../src/graphql/resolvers/__init__.py | 7 +- .../src/graphql/resolvers/form_resolvers.py | 51 ++++++++ .../src/graphql/schema/__init__.py | 14 ++- .../src/graphql/schema/form_schema.py | 46 +++++-- .../src/graphql/service/__init__.py | 3 +- .../src/graphql/service/base_service.py | 42 +++++++ .../src/graphql/service/form_service.py | 118 ++++++------------ forms-flow-data-layer/src/middlewares/auth.py | 17 ++- .../src/middlewares/role_check.py | 2 + .../src/models/formio/__init__.py | 4 +- .../src/models/formio/form.py | 14 +-- .../src/models/webapi/__init__.py | 4 +- .../src/models/webapi/application.py | 12 +- .../src/models/webapi/authorization.py | 12 +- .../src/models/webapi/base.py | 28 ++++- .../src/models/webapi/constants.py | 9 -- .../src/models/webapi/form_process_mapper.py | 13 ++ .../src/models/webapi/formprocess_mapper.py | 20 --- 19 files changed, 266 insertions(+), 154 deletions(-) create mode 100644 forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py create mode 100644 forms-flow-data-layer/src/graphql/service/base_service.py delete mode 100644 forms-flow-data-layer/src/models/webapi/constants.py create mode 100644 forms-flow-data-layer/src/models/webapi/form_process_mapper.py delete mode 100644 forms-flow-data-layer/src/models/webapi/formprocess_mapper.py diff --git a/forms-flow-data-layer/src/db/formio_db.py b/forms-flow-data-layer/src/db/formio_db.py index a12d4ccc26..6d7be289d8 100644 --- a/forms-flow-data-layer/src/db/formio_db.py +++ b/forms-flow-data-layer/src/db/formio_db.py @@ -4,7 +4,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from src.config.envs import ENVS -from src.models.formio import FormModel, SubmissionsModel # Import your MongoDB models +from src.models.formio import Form, SubmissionsModel # Import your MongoDB models from src.utils import get_logger logger = get_logger(__name__) @@ -23,7 +23,7 @@ async def init_formio_db(self): self.__client = AsyncIOMotorClient(ENVS.FORMIO_MONGO_DB_URI) self.formio_db = self.__client[ENVS.FORMIO_DB_NAME] await init_beanie( - database=self.formio_db, document_models=[FormModel, SubmissionsModel] + database=self.formio_db, document_models=[Form, SubmissionsModel] ) def get_db(self): diff --git a/forms-flow-data-layer/src/graphql/resolvers/__init__.py b/forms-flow-data-layer/src/graphql/resolvers/__init__.py index 348fb85eaf..fed9ed2f03 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/__init__.py +++ b/forms-flow-data-layer/src/graphql/resolvers/__init__.py @@ -1,10 +1,9 @@ import strawberry -from src.graphql.resolvers.submission_resolvers import ( - QuerySubmissionsResolver, -) +from src.graphql.resolvers.form_resolvers import QueryFormsResolver +from src.graphql.resolvers.submission_resolvers import QuerySubmissionsResolver @strawberry.type -class Query(QuerySubmissionsResolver): # Inherit from query classes +class Query(QuerySubmissionsResolver, QueryFormsResolver): # Inherit from query classes pass diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py new file mode 100644 index 0000000000..2847a69bc6 --- /dev/null +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -0,0 +1,51 @@ +from typing import Optional + +import strawberry + +from src.graphql.schema import FormSchema, PaginationWindow +from src.graphql.service import FormService +from src.middlewares.auth import auth + + +@strawberry.type +class QueryFormsResolver: + @strawberry.field(extensions=[auth.auth_required()]) + @strawberry.field + async def get_forms( + self, + order_by: str = 'id', + limit: int = 100, + offset: int = 0, + type: Optional[str] = None + ) -> PaginationWindow[FormSchema]: + filters = {} + + if type: + filters["type"] = type + + forms = await FormService.get_forms( + order_by=order_by, + limit=limit, + offset=offset, + filters=filters + ) + return forms + + @strawberry.field(extensions=[auth.auth_required()]) + @strawberry.field + async def get_form( + self, + form_id: str, + ) -> Optional[FormSchema]: + """ + GraphQL resolver for querying form. + + Args: + form_id (str): ID of the form + Returns: + Form object containing combined SQL and MongoDB data + """ + form = await FormService.get_form( + form_id=form_id, + ) + return form diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 5ed7b5b891..6258da1bff 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,12 +1,24 @@ -from src.graphql.schema.form_schema import FormSchema +from typing import List, TypeVar, Generic + +import strawberry + +from src.graphql.schema.form_schema import (FormSchema) from src.graphql.schema.submission_schema import ( PaginatedSubmissionResponse, SubmissionDetailsWithSubmissionData, SubmissionSchema, ) +# Generic pagination window +Item = TypeVar("Item") +@strawberry.type +class PaginationWindow(Generic[Item]): + items: List[Item] + total_count: int + __all__ = [ "FormSchema", + "PaginationWindow", "SubmissionSchema", "SubmissionDetailsWithSubmissionData", "PaginatedSubmissionResponse", diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index d1622499a5..941cde6213 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -1,10 +1,8 @@ -from typing import Optional +from typing import Any, Optional import strawberry -from src.middlewares.role_check import RoleCheck - -# currently this file is not used in the codebase, but it is kept for future use +from src.middlewares.auth import IsAdmin @strawberry.type @@ -14,13 +12,47 @@ class FormSchema: This is the external representation of your database model """ + # FormIO populated fields id: str - name: Optional[str] = strawberry.field( - extensions=[RoleCheck(["admin"])] - ) # Add this line title: str + name: Optional[str] = strawberry.field(permission_classes=[IsAdmin]) path: str type: str display: Optional[str] = None + parent_form_id: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None + + # WebAPI populated fields + created_by: str + updated_by: str + status: str + version: int + + # BPM populated fields + # None + + # Calculated fields + total_submissions: int + + @staticmethod + def from_result(result: dict): + formio = result["formio"] + webapi = result["webapi"] + calculated = result["calculated"] + return FormSchema( + id=formio.id, + title=formio.title, + name=formio.name, + path=formio.path, + type=formio.type, + display=formio.display, + parent_form_id=formio.parentFormId, + created_at=(formio.created.isoformat() if formio.created else None), + updated_at=(formio.modified.isoformat() if formio.modified else None), + created_by=webapi.created_by, + updated_by=webapi.modified_by, + status=webapi.status, + version=webapi.version, + total_submissions=calculated["total_submissions"] + ) diff --git a/forms-flow-data-layer/src/graphql/service/__init__.py b/forms-flow-data-layer/src/graphql/service/__init__.py index 39470e71f4..fb1a9288c3 100644 --- a/forms-flow-data-layer/src/graphql/service/__init__.py +++ b/forms-flow-data-layer/src/graphql/service/__init__.py @@ -1,4 +1,5 @@ +from src.graphql.service.base_service import BaseService from src.graphql.service.form_service import FormService from src.graphql.service.submission_service import SubmissionService -__all__ = ["FormService", "SubmissionService"] +__all__ = ["BaseService", "FormService", "SubmissionService"] diff --git a/forms-flow-data-layer/src/graphql/service/base_service.py b/forms-flow-data-layer/src/graphql/service/base_service.py new file mode 100644 index 0000000000..1a7083456f --- /dev/null +++ b/forms-flow-data-layer/src/graphql/service/base_service.py @@ -0,0 +1,42 @@ +from typing import List, TypeVar, Generic + +from src.utils import get_logger + +logger = get_logger(__name__) +MongoModel = TypeVar("MongoModel") +PostgresModel = TypeVar("PostgresModel") + + +class BaseService(): + @staticmethod + async def _formio_find_all( + model: Generic[MongoModel], + limit: int = None, + offset: int = None, + filters: dict = None + ) -> List[MongoModel]: + query = model.find_all() + + # Apply filters + for filter, value in filters.items(): + if hasattr(model, filter): + query = query.find(getattr(model, filter) == value) + total_count = await query.count() + + # Apply pagination + results = query.skip(offset).limit(limit) + return results, total_count + + @staticmethod + async def _webapi_find_all( + model: Generic[PostgresModel], + limit: int = None, + offset: int = None, + filters: dict = None + ) -> List[PostgresModel]: + query = await model.find_all(**filters) + total_count = len((await model.execute(query)).all()) + + # Apply pagination + results = (await model.execute(query.offset(offset).limit(limit))) + return results, total_count diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 146ba09ea9..aaf397f777 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -2,94 +2,58 @@ from beanie import PydanticObjectId -from src.graphql.schema import FormSchema -from src.models.formio import FormModel +from src.graphql.service import BaseService +from src.graphql.schema import FormSchema, PaginationWindow +from src.models.formio import Form, SubmissionsModel +from src.models.webapi import FormProcessMapper from src.utils import get_logger logger = get_logger(__name__) -# this is not used for now, but we can use it in the future if needed - # Service Layer for Form-related Operations -class FormService: - @staticmethod - def convert_to_graphql_type(form_model: FormModel) -> FormSchema: - """ - Convert a Beanie FormModel to a GraphQL FormSchema - - Args: - form_model (FormModel): Database model to convert - - Returns: - FormSchema: GraphQL type representation - """ - return FormSchema( - id=str(form_model.id), # Convert ObjectId to string - name=form_model.name, - path=form_model.path, - type=form_model.type, - title=form_model.title, - display=str(form_model.display) if form_model.display else None, - created_at=( - form_model.created_at.isoformat() if form_model.created_at else None - ), - updated_at=( - form_model.updated_at.isoformat() if form_model.updated_at else None - ), - ) - - @staticmethod +class FormService(BaseService): + @classmethod async def get_forms( - skip: int = 0, limit: int = 100, type_filter: Optional[str] = None - ) -> List[FormSchema]: - """ - Fetch and convert forms to GraphQL types + cls, + order_by: str, + limit: int = 100, + offset: int = 0, + filters: dict[str, str] = {}, + ) -> PaginationWindow[FormSchema]: + # Query webapi database + webapi_query, webapi_total_count = await cls._webapi_find_all(FormProcessMapper, limit, offset, filters) + + # Combine results with data from formio + results = [] + webapi_results = webapi_query.all() + for wr in webapi_results: + _, submissions_count = await cls._formio_find_all(SubmissionsModel, filters={"form": PydanticObjectId(wr.form_id)}) + results.append({ + "webapi": wr, + "formio": await Form.get(PydanticObjectId(wr.form_id)), + "calculated": {"total_submissions": submissions_count} + }) + + # Convert to GraphQL Schema + forms = [FormSchema.from_result(result=r) for r in results] + forms.sort(key=lambda x: getattr(x, order_by)) + return PaginationWindow(items=forms, total_count=webapi_total_count) - Args: - skip (int): Pagination - number of items to skip - limit (int): Maximum number of items to return - type_filter (Optional[str]): Optional filter by form type - - Returns: - List[FormSchema]: List of converted GraphQL form types - """ - query = FormModel.find_all() - - if type_filter: - query = query.find(FormModel.type == type_filter) - - # Execute query and convert results - forms = await query.skip(skip).limit(limit).to_list() - - # Convert each form to GraphQL type - return [FormService.convert_to_graphql_type(form) for form in forms] @staticmethod async def get_form(form_id: str) -> Optional[FormSchema]: - """ - service to fetch a single form by ID - - Args: - form_id (str): ID of the form to fetch - - Returns: - Optional[FormSchema]: Matching form or None - """ - try: - # Convert string ID back to PydanticObjectId - object_id = PydanticObjectId(form_id) + # Query the databases + webapi_result = await FormProcessMapper.first(form_id=form_id) + formio_result = await Form.get(PydanticObjectId(form_id)) - # Fetch the form - form_model = await FormModel.find_one(id=object_id) + # Combine results + result = { + "webapi": webapi_result, + "formio": formio_result + } - # If found, convert to GraphQL type - return ( - await FormService.convert_to_graphql_type(form_model) - if form_model - else None - ) + # Convert to GraphQL Schema + form = FormSchema.from_result(result=result) + return form - except Exception: - # Handle invalid ID format or not found scenarios - return None diff --git a/forms-flow-data-layer/src/middlewares/auth.py b/forms-flow-data-layer/src/middlewares/auth.py index 2c36a1b267..e9251d081c 100644 --- a/forms-flow-data-layer/src/middlewares/auth.py +++ b/forms-flow-data-layer/src/middlewares/auth.py @@ -43,6 +43,22 @@ async def has_permission( raise GraphQLError(f"Unexpected error: {str(e)}") +class IsAdmin(BasePermission): + """Class for check if user is admin.""" + + message = "User role must be admin" + error_extensions = {"code": "UNAUTHORIZED"} + + async def has_permission( + self, source: Any, info: strawberry.Info, **kwargs + ) -> bool: + try: + user = info.context["user"] + return user.has_any_roles(['admin']) + except Exception as e: + raise GraphQLError(f"Unexpected error: {str(e)}") + + class HasAnyRole(BasePermission): """Class for check authorization.""" @@ -63,7 +79,6 @@ async def has_permission( class Auth: - @staticmethod def auth_required(roles: List[str] = None): """ diff --git a/forms-flow-data-layer/src/middlewares/role_check.py b/forms-flow-data-layer/src/middlewares/role_check.py index 3930a7d0d8..7d6232b589 100644 --- a/forms-flow-data-layer/src/middlewares/role_check.py +++ b/forms-flow-data-layer/src/middlewares/role_check.py @@ -3,6 +3,8 @@ import strawberry from strawberry.extensions import FieldExtension +# Currently not used in favour of extending BasePermission in auth.py +# See here for more details: https://strawberry.rocks/docs/guides/permissions class RoleCheck(FieldExtension): """Custom extension.""" diff --git a/forms-flow-data-layer/src/models/formio/__init__.py b/forms-flow-data-layer/src/models/formio/__init__.py index f4e09d591b..5d38908f1b 100644 --- a/forms-flow-data-layer/src/models/formio/__init__.py +++ b/forms-flow-data-layer/src/models/formio/__init__.py @@ -1,5 +1,5 @@ from src.models.formio.constants import FormioTables -from src.models.formio.form import FormModel +from src.models.formio.form import Form from src.models.formio.submission import SubmissionsModel -__all__ = ["FormModel", "SubmissionsModel", "FormioTables"] +__all__ = ["Form", "SubmissionsModel", "FormioTables"] diff --git a/forms-flow-data-layer/src/models/formio/form.py b/forms-flow-data-layer/src/models/formio/form.py index 81573e7de1..e472a200df 100644 --- a/forms-flow-data-layer/src/models/formio/form.py +++ b/forms-flow-data-layer/src/models/formio/form.py @@ -1,21 +1,21 @@ +from datetime import datetime from typing import Optional from beanie import Document from .constants import FormioTables -# currently this file is not used in the codebase, but it is kept for future use - -class FormModel(Document): +class Form(Document): title: str name: str path: str type: str - isBundle: Optional[bool] - display: Optional[str] = None - created_at: Optional[str] = None - updated_at: Optional[str] = None + isBundle: bool + display: Optional[str] = None + created: Optional[datetime] = None + modified: Optional[datetime] = None + parentFormId: Optional[str] = None class Settings: name = FormioTables.FORMS.value diff --git a/forms-flow-data-layer/src/models/webapi/__init__.py b/forms-flow-data-layer/src/models/webapi/__init__.py index af0ff0f4c4..c886f57ab8 100644 --- a/forms-flow-data-layer/src/models/webapi/__init__.py +++ b/forms-flow-data-layer/src/models/webapi/__init__.py @@ -1,13 +1,11 @@ from src.models.webapi.application import Application from src.models.webapi.authorization import Authorization from src.models.webapi.base import BaseModel -from src.models.webapi.constants import WebApiTables -from src.models.webapi.formprocess_mapper import FormProcessMapper +from src.models.webapi.form_process_mapper import FormProcessMapper __all__ = [ "Authorization", "Application", "FormProcessMapper", - "WebApiTables", "BaseModel", ] diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index d25252bb8b..2a2164b31a 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -6,8 +6,7 @@ from .authorization import Authorization, AuthType from .base import BaseModel -from .constants import WebApiTables -from .formprocess_mapper import FormProcessMapper +from .form_process_mapper import FormProcessMapper class Application(BaseModel): @@ -16,13 +15,8 @@ class Application(BaseModel): This class provides methods to interact with the application table. """ - _application = None - - @classmethod - async def get_table(cls): - if cls._application is None: - cls._application = await webapi_db.get_table(WebApiTables.APPLICATION.value) - return cls._application + _table_name = "application" + _table = None @classmethod def filter_query(cls, query, filter_data: dict, application_table, mapper_table): diff --git a/forms-flow-data-layer/src/models/webapi/authorization.py b/forms-flow-data-layer/src/models/webapi/authorization.py index 7335a49f72..279edeeea8 100644 --- a/forms-flow-data-layer/src/models/webapi/authorization.py +++ b/forms-flow-data-layer/src/models/webapi/authorization.py @@ -5,7 +5,6 @@ from src.db.webapi_db import webapi_db from .base import BaseModel -from .constants import WebApiTables class AuthType(Enum): @@ -19,15 +18,8 @@ class Authorization(BaseModel): Authorization class to handle authorization-related information. """ - _authorization_table = None # Class-level cache - - @classmethod - async def get_table(cls): - if cls._authorization_table is None: - cls._authorization_table = await webapi_db.get_table( - WebApiTables.AUTHORIZATION.value - ) - return cls._authorization_table + _table_name = "authorization" + _table = None # Class-level cache @classmethod async def get_role_conditions(cls, authorization_table, roles: list[str]): diff --git a/forms-flow-data-layer/src/models/webapi/base.py b/forms-flow-data-layer/src/models/webapi/base.py index 3841e0f6b3..ad0ff4724e 100644 --- a/forms-flow-data-layer/src/models/webapi/base.py +++ b/forms-flow-data-layer/src/models/webapi/base.py @@ -1,5 +1,5 @@ from src.db.webapi_db import webapi_db - +from sqlalchemy import select class BaseModel: """Base class for webapi session management.""" @@ -22,3 +22,29 @@ async def execute(cls, query): """ async with webapi_db.get_session() as session: return await session.execute(query) + + @classmethod + async def get_table(cls): + if cls._table is None: + cls._table = await webapi_db.get_table(cls._table_name) + return cls._table + + @classmethod + async def first(cls, **filters): + """ + Find the first entries that match the passed args. + """ + stmt = await cls.find_all(**filters) + return (await cls.execute(stmt)).first() + + @classmethod + async def find_all(cls, **filters): + """ + Find all entries that match the passed args. + """ + table = await cls.get_table() + stmt = select(table) + for key, value in filters.items(): + if hasattr(table.c, key): + stmt = stmt.where(getattr(table.c, key) == value) + return stmt diff --git a/forms-flow-data-layer/src/models/webapi/constants.py b/forms-flow-data-layer/src/models/webapi/constants.py deleted file mode 100644 index 2ed2fd41ce..0000000000 --- a/forms-flow-data-layer/src/models/webapi/constants.py +++ /dev/null @@ -1,9 +0,0 @@ -from enum import Enum - - -class WebApiTables(Enum): - """Enum for managing Web API table names.""" - - APPLICATION = "application" - AUTHORIZATION = "authorization" - FORM_PROCESS_MAPPER = "form_process_mapper" diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py new file mode 100644 index 0000000000..87bc415ea0 --- /dev/null +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -0,0 +1,13 @@ +from src.db.webapi_db import webapi_db + +from .base import BaseModel + + +class FormProcessMapper(BaseModel): + """ + FormProcessMapper class to handle mapper-related information. + """ + + _table_name = "form_process_mapper" + _table = None # cache for the mapper table + diff --git a/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py b/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py deleted file mode 100644 index 326be3cde4..0000000000 --- a/forms-flow-data-layer/src/models/webapi/formprocess_mapper.py +++ /dev/null @@ -1,20 +0,0 @@ -from src.db.webapi_db import webapi_db - -from .base import BaseModel -from .constants import WebApiTables - - -class FormProcessMapper(BaseModel): - """ - FormProcessMapper class to handle mapper-related information. - """ - - _mapper = None # cache for the mapper table - - @classmethod - async def get_table(cls): - if cls._mapper is None: - cls._mapper = await webapi_db.get_table( - WebApiTables.FORM_PROCESS_MAPPER.value - ) - return cls._mapper From 9dfedc525e9bced956168e2f5b2b2a5dbc85c336 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 21 Jul 2025 21:41:00 -0700 Subject: [PATCH 05/16] Add comments to added form graphql query --- .../src/graphql/resolvers/form_resolvers.py | 15 +++++++++--- .../graphql/resolvers/submission_resolvers.py | 2 +- .../src/graphql/schema/__init__.py | 6 ++--- .../src/graphql/service/base_service.py | 2 +- .../src/graphql/service/form_service.py | 24 +++++++++++++++++-- .../src/models/webapi/__init__.py | 2 ++ .../src/models/webapi/application.py | 3 ++- .../src/models/webapi/authorization.py | 4 ++-- .../src/models/webapi/base.py | 17 ++++++------- .../src/models/webapi/constants.py | 9 +++++++ .../src/models/webapi/form_process_mapper.py | 4 ++-- 11 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 forms-flow-data-layer/src/models/webapi/constants.py diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py index 2847a69bc6..f34613091e 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -10,7 +10,6 @@ @strawberry.type class QueryFormsResolver: @strawberry.field(extensions=[auth.auth_required()]) - @strawberry.field async def get_forms( self, order_by: str = 'id', @@ -18,6 +17,17 @@ async def get_forms( offset: int = 0, type: Optional[str] = None ) -> PaginationWindow[FormSchema]: + """ + GraphQL resolver for querying forms. + + Args: + order_by (str): Field to sort by (default: 'id') + limit (int): Number of items to return (default: 100) + offset (int): Pagination offset (default: 0) + type (Optional[str]): Type of form to return + Returns: + Paginated list of Form objects containing combined PostgreSQL and MongoDB data + """ filters = {} if type: @@ -32,7 +42,6 @@ async def get_forms( return forms @strawberry.field(extensions=[auth.auth_required()]) - @strawberry.field async def get_form( self, form_id: str, @@ -43,7 +52,7 @@ async def get_form( Args: form_id (str): ID of the form Returns: - Form object containing combined SQL and MongoDB data + Form object containing combined PostgreSQL and MongoDB data """ form = await FormService.get_form( form_id=form_id, diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index 69cb9b15bf..513b889c87 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -10,7 +10,7 @@ @strawberry.type class QuerySubmissionsResolver: - @strawberry.field(extensions=[auth.auth_required()]) + # @strawberry.field(extensions=[auth.auth_required()]) async def get_submission( self, info: strawberry.Info, diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 6258da1bff..2d6fca4e77 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,18 +1,18 @@ -from typing import List, TypeVar, Generic +from typing import Generic, List, TypeVar import strawberry -from src.graphql.schema.form_schema import (FormSchema) +from src.graphql.schema.form_schema import FormSchema from src.graphql.schema.submission_schema import ( PaginatedSubmissionResponse, SubmissionDetailsWithSubmissionData, SubmissionSchema, ) -# Generic pagination window Item = TypeVar("Item") @strawberry.type class PaginationWindow(Generic[Item]): + """GraphQL type representing a generic set of paginated items.""" items: List[Item] total_count: int diff --git a/forms-flow-data-layer/src/graphql/service/base_service.py b/forms-flow-data-layer/src/graphql/service/base_service.py index 1a7083456f..ae4a4b0035 100644 --- a/forms-flow-data-layer/src/graphql/service/base_service.py +++ b/forms-flow-data-layer/src/graphql/service/base_service.py @@ -1,4 +1,4 @@ -from typing import List, TypeVar, Generic +from typing import Generic, List, TypeVar from src.utils import get_logger diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index aaf397f777..ad20463a3f 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -2,8 +2,8 @@ from beanie import PydanticObjectId -from src.graphql.service import BaseService from src.graphql.schema import FormSchema, PaginationWindow +from src.graphql.service import BaseService from src.models.formio import Form, SubmissionsModel from src.models.webapi import FormProcessMapper from src.utils import get_logger @@ -11,8 +11,9 @@ logger = get_logger(__name__) -# Service Layer for Form-related Operations class FormService(BaseService): + """Service class for handling form related operations.""" + @classmethod async def get_forms( cls, @@ -21,6 +22,17 @@ async def get_forms( offset: int = 0, filters: dict[str, str] = {}, ) -> PaginationWindow[FormSchema]: + """ + Fetches forms from the WebAPI and adds additional details from FormIO. + + Args: + order_by (str): Field to sort by (default: 'id') + limit (int): Number of items to return (default: 100) + offset (int): Pagination offset (default: 0) + filters (dict): Search filters to apply to the query + Returns: + Paginated list of Form objects containing combined PostgreSQL and MongoDB data + """ # Query webapi database webapi_query, webapi_total_count = await cls._webapi_find_all(FormProcessMapper, limit, offset, filters) @@ -43,6 +55,14 @@ async def get_forms( @staticmethod async def get_form(form_id: str) -> Optional[FormSchema]: + """ + Fetches a form based on it's form_id from the WebAPI and adds additional details from FormIO. + + Args: + form_id (str): ID of the form + Returns: + Form object containing combined PostgreSQL and MongoDB data + """ # Query the databases webapi_result = await FormProcessMapper.first(form_id=form_id) formio_result = await Form.get(PydanticObjectId(form_id)) diff --git a/forms-flow-data-layer/src/models/webapi/__init__.py b/forms-flow-data-layer/src/models/webapi/__init__.py index c886f57ab8..231dc73fca 100644 --- a/forms-flow-data-layer/src/models/webapi/__init__.py +++ b/forms-flow-data-layer/src/models/webapi/__init__.py @@ -1,11 +1,13 @@ from src.models.webapi.application import Application from src.models.webapi.authorization import Authorization from src.models.webapi.base import BaseModel +from src.models.webapi.constants import WebApiTables from src.models.webapi.form_process_mapper import FormProcessMapper __all__ = [ "Authorization", "Application", "FormProcessMapper", + "WebApiTables", "BaseModel", ] diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 2a2164b31a..61cbd22fac 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -6,6 +6,7 @@ from .authorization import Authorization, AuthType from .base import BaseModel +from .constants import WebApiTables from .form_process_mapper import FormProcessMapper @@ -15,7 +16,7 @@ class Application(BaseModel): This class provides methods to interact with the application table. """ - _table_name = "application" + _table_name = WebApiTables.APPLICATION.value _table = None @classmethod diff --git a/forms-flow-data-layer/src/models/webapi/authorization.py b/forms-flow-data-layer/src/models/webapi/authorization.py index 279edeeea8..1a222741f6 100644 --- a/forms-flow-data-layer/src/models/webapi/authorization.py +++ b/forms-flow-data-layer/src/models/webapi/authorization.py @@ -5,7 +5,7 @@ from src.db.webapi_db import webapi_db from .base import BaseModel - +from .constants import WebApiTables class AuthType(Enum): APPLICATION = "APPLICATION" @@ -18,7 +18,7 @@ class Authorization(BaseModel): Authorization class to handle authorization-related information. """ - _table_name = "authorization" + _table_name = WebApiTables.AUTHORIZATION.value _table = None # Class-level cache @classmethod diff --git a/forms-flow-data-layer/src/models/webapi/base.py b/forms-flow-data-layer/src/models/webapi/base.py index ad0ff4724e..04eda77cab 100644 --- a/forms-flow-data-layer/src/models/webapi/base.py +++ b/forms-flow-data-layer/src/models/webapi/base.py @@ -1,6 +1,8 @@ -from src.db.webapi_db import webapi_db from sqlalchemy import select +from src.db.webapi_db import webapi_db + + class BaseModel: """Base class for webapi session management.""" @@ -8,9 +10,7 @@ class BaseModel: @classmethod async def _get_session(cls): - """ - Returns the webapi session object. - """ + """Returns the webapi session object.""" if cls._webapi_session is None: cls._webapi_session = await webapi_db.get_session() return cls._webapi_session @@ -25,23 +25,20 @@ async def execute(cls, query): @classmethod async def get_table(cls): + """Gets and caches a SQLAlchemy table.""" if cls._table is None: cls._table = await webapi_db.get_table(cls._table_name) return cls._table @classmethod async def first(cls, **filters): - """ - Find the first entries that match the passed args. - """ + """Find the first entries that match the passed args.""" stmt = await cls.find_all(**filters) return (await cls.execute(stmt)).first() @classmethod async def find_all(cls, **filters): - """ - Find all entries that match the passed args. - """ + """Find all entries that match the passed args.""" table = await cls.get_table() stmt = select(table) for key, value in filters.items(): diff --git a/forms-flow-data-layer/src/models/webapi/constants.py b/forms-flow-data-layer/src/models/webapi/constants.py new file mode 100644 index 0000000000..2ed2fd41ce --- /dev/null +++ b/forms-flow-data-layer/src/models/webapi/constants.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class WebApiTables(Enum): + """Enum for managing Web API table names.""" + + APPLICATION = "application" + AUTHORIZATION = "authorization" + FORM_PROCESS_MAPPER = "form_process_mapper" diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py index 87bc415ea0..c01a40e480 100644 --- a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -1,13 +1,13 @@ from src.db.webapi_db import webapi_db from .base import BaseModel - +from .constants import WebApiTables class FormProcessMapper(BaseModel): """ FormProcessMapper class to handle mapper-related information. """ - _table_name = "form_process_mapper" + _table_name = WebApiTables.FORM_PROCESS_MAPPER.value _table = None # cache for the mapper table From ef1529f97ec50f7706816fdf0ba559f183607f0c Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 21 Jul 2025 22:03:58 -0700 Subject: [PATCH 06/16] Rename SubmissionModel to Submission --- forms-flow-data-layer/src/db/formio_db.py | 4 +- .../graphql/resolvers/submission_resolvers.py | 35 ++++++++++++++ .../src/graphql/schema/form_schema.py | 8 ++-- .../src/graphql/schema/submission_schema.py | 38 ++++++++++++++- .../src/graphql/service/form_service.py | 4 +- .../src/graphql/service/submission_service.py | 46 +++++++++++++++++-- .../src/models/formio/__init__.py | 4 +- .../src/models/formio/submission.py | 19 +++----- 8 files changed, 131 insertions(+), 27 deletions(-) diff --git a/forms-flow-data-layer/src/db/formio_db.py b/forms-flow-data-layer/src/db/formio_db.py index 6d7be289d8..66d3d8c50e 100644 --- a/forms-flow-data-layer/src/db/formio_db.py +++ b/forms-flow-data-layer/src/db/formio_db.py @@ -4,7 +4,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from src.config.envs import ENVS -from src.models.formio import Form, SubmissionsModel # Import your MongoDB models +from src.models.formio import Form, Submission # Import your MongoDB models from src.utils import get_logger logger = get_logger(__name__) @@ -23,7 +23,7 @@ async def init_formio_db(self): self.__client = AsyncIOMotorClient(ENVS.FORMIO_MONGO_DB_URI) self.formio_db = self.__client[ENVS.FORMIO_DB_NAME] await init_beanie( - database=self.formio_db, document_models=[Form, SubmissionsModel] + database=self.formio_db, document_models=[Form, Submission] ) def get_db(self): diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index 513b889c87..0029f77b63 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -3,6 +3,7 @@ import strawberry from strawberry.scalars import JSON +from src.graphql.schema import PaginationWindow, SubmissionSchema from src.graphql.schema import PaginatedSubmissionResponse from src.graphql.service import SubmissionService from src.middlewares.auth import auth @@ -11,6 +12,40 @@ @strawberry.type class QuerySubmissionsResolver: # @strawberry.field(extensions=[auth.auth_required()]) + @strawberry.field + async def get_submissions( + self, + order_by: str = 'id', + limit: int = 100, + offset: int = 0, + form_name: Optional[str] = None, + ) -> PaginationWindow[SubmissionSchema]: + """ + GraphQL resolver for querying submissions. + + Args: + order_by (str): Field to sort by (default: 'id') + limit (int): Number of items to return (default: 100) + offset (int): Pagination offset (default: 0) + form_name (Optional[str]): Name of the form to get submissions from + Returns: + Paginated list of Submission objects containing combined PostgreSQL and MongoDB data + """ + filters = {} + + if form_name: + filters["form_name"] = form_name + + forms = await SubmissionService.get_submissions( + order_by=order_by, + limit=limit, + offset=offset, + filters=filters + ) + return forms + + # @strawberry.field(extensions=[auth.auth_required()]) + @strawberry.field async def get_submission( self, info: strawberry.Info, diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index 941cde6213..49dd8a2cbb 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -21,11 +21,11 @@ class FormSchema: display: Optional[str] = None parent_form_id: Optional[str] = None created_at: Optional[str] = None - updated_at: Optional[str] = None + modified_at: Optional[str] = None # WebAPI populated fields created_by: str - updated_by: str + modified_by: str status: str version: int @@ -49,9 +49,9 @@ def from_result(result: dict): display=formio.display, parent_form_id=formio.parentFormId, created_at=(formio.created.isoformat() if formio.created else None), - updated_at=(formio.modified.isoformat() if formio.modified else None), + modified_at=(formio.modified.isoformat() if formio.modified else None), created_by=webapi.created_by, - updated_by=webapi.modified_by, + modified_by=webapi.modified_by, status=webapi.status, version=webapi.version, total_submissions=calculated["total_submissions"] diff --git a/forms-flow-data-layer/src/graphql/schema/submission_schema.py b/forms-flow-data-layer/src/graphql/schema/submission_schema.py index 1ce4b436b5..62766fe377 100644 --- a/forms-flow-data-layer/src/graphql/schema/submission_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/submission_schema.py @@ -9,15 +9,49 @@ @strawberry.type class SubmissionSchema: """ - GraphQL type representing a Application + GraphQL type representing a Submission This is the external representation of your database model """ + # WebAPI populated fields id: int application_status: str - task_name: str + form_id: str + submission_id: str + created_at: str + updated_at: str + created_by: str + updated_by: str + is_resubmit: bool + is_draft: bool + + # FormIO populated fields data: Optional[strawberry.scalars.JSON] # Field to hold arbitrary JSON data + # BPM populated fields + # None + + # Calculated fields + # None + + @staticmethod + def from_result(result: dict): + formio = result["formio"] + webapi = result["webapi"] + return SubmissionSchema( + id=webapi.id, + application_status=webapi.application_status, + form_id=webapi.latest_form_id, + submission_id=webapi.submission_id, + created_at=(webapi.created.isoformat() if webapi.created else None), + updated_at=(webapi.modified.isoformat() if webapi.modified else None), + created_by=webapi.created_by, + updated_by=webapi.modified_by, + is_resubmit=webapi.is_resubmit, + is_draft=webapi.is_draft, + data=formio.data + ) + @strawberry.type class SubmissionDetailsWithSubmissionData: diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index ad20463a3f..65b0cd1bfb 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -4,7 +4,7 @@ from src.graphql.schema import FormSchema, PaginationWindow from src.graphql.service import BaseService -from src.models.formio import Form, SubmissionsModel +from src.models.formio import Form, Submission from src.models.webapi import FormProcessMapper from src.utils import get_logger @@ -40,7 +40,7 @@ async def get_forms( results = [] webapi_results = webapi_query.all() for wr in webapi_results: - _, submissions_count = await cls._formio_find_all(SubmissionsModel, filters={"form": PydanticObjectId(wr.form_id)}) + _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(wr.form_id)}) results.append({ "webapi": wr, "formio": await Form.get(PydanticObjectId(wr.form_id)), diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index 00c680559f..a1d3210ddc 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -1,13 +1,15 @@ from typing import Any, Dict, List, Optional, Tuple import strawberry +from beanie import PydanticObjectId +from src.graphql.schema import PaginationWindow, SubmissionSchema from src.graphql.schema import ( PaginatedSubmissionResponse, SubmissionDetailsWithSubmissionData, ) -from src.models.formio.submission import SubmissionsModel -from src.models.webapi.application import Application +from src.models.formio import Submission +from src.models.webapi import Application from src.utils import get_logger logger = get_logger(__name__) @@ -16,6 +18,44 @@ class SubmissionService: """Service class for handling submission related operations on mongo and webapi side.""" + @classmethod + async def get_submissions( + cls, + order_by: str, + limit: int = 100, + offset: int = 0, + filters: dict[str, str] = {}, + ) -> PaginationWindow[SubmissionSchema]: + """ + Fetches submissions from the WebAPI and adds additional details from FormIO. + + Args: + order_by (str): Field to sort by (default: 'id') + limit (int): Number of items to return (default: 100) + offset (int): Pagination offset (default: 0) + filters (dict): Search filters to apply to the query + Returns: + Paginated list of Submission objects containing combined PostgreSQL and MongoDB data + """ + # Query webapi database + webapi_query, webapi_total_count = await cls._webapi_find_all(Application, limit, offset, filters) + + # Combine results with data from formio + results = [] + webapi_results = webapi_query.all() + for wr in webapi_results: + _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(wr.form_id)}) + results.append({ + "webapi": wr, + "formio": await Submission.get(PydanticObjectId(wr.form_id)), + "calculated": {"total_submissions": submissions_count} + }) + + # Convert to GraphQL Schema + forms = [SubmissionSchema.from_result(result=r) for r in results] + forms.sort(key=lambda x: getattr(x, order_by)) + return PaginationWindow(items=forms, total_count=webapi_total_count) + @staticmethod async def split_search_criteria( search: Optional[Dict[str, Any]], webapi_fields: List[str] @@ -160,7 +200,7 @@ async def get_submission( if app["submission_id"] ] # Get filtered submissions from MongoDB - mongo_side_submissions = await SubmissionsModel.query_submission( + mongo_side_submissions = await Submission.query_submission( submission_ids=submission_ids, filter=mongo_search, selected_form_fields=selected_form_fields, diff --git a/forms-flow-data-layer/src/models/formio/__init__.py b/forms-flow-data-layer/src/models/formio/__init__.py index 5d38908f1b..78363704f8 100644 --- a/forms-flow-data-layer/src/models/formio/__init__.py +++ b/forms-flow-data-layer/src/models/formio/__init__.py @@ -1,5 +1,5 @@ from src.models.formio.constants import FormioTables from src.models.formio.form import Form -from src.models.formio.submission import SubmissionsModel +from src.models.formio.submission import Submission -__all__ = ["Form", "SubmissionsModel", "FormioTables"] +__all__ = ["Form", "Submission", "FormioTables"] diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index 727d5ff648..3eaeb18045 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -6,9 +6,8 @@ from .constants import FormioTables -class SubmissionsModel(Document): +class Submission(Document): data: dict - _id: PydanticObjectId class Settings: name = FormioTables.SUBMISSIONS.value @@ -63,29 +62,25 @@ async def query_submission( Query submissions from MongoDB with optional pagination and sorting. """ # Build match stage - match_stage = SubmissionsModel._build_match_stage( + match_stage = Submission._build_match_stage( submission_ids=submission_ids, filter=filter ) pipeline = [{"$match": match_stage}] # Add sorting if sort_by is specified - if sort_stage := SubmissionsModel._build_sort_stage(sort_by, sort_order): + if sort_stage := Submission._build_sort_stage(sort_by, sort_order): pipeline.append(sort_stage) # Projection stage - pipeline.append(SubmissionsModel._build_projection_stage(selected_form_fields)) + pipeline.append(Submission._build_projection_stage(selected_form_fields)) # Only add pagination if page_no and limit specified if page_no is not None and limit is not None: - # Get only the count (no document data) - count_pipeline = pipeline + [{"$count": "total"}] - count_result = await SubmissionsModel.aggregate(count_pipeline).to_list(length=1) - total = count_result[0]["total"] if count_result else 0 - # Add skip and limit stages for pagination + total = await Submission.aggregate(pipeline).count() pipeline.append({"$skip": (page_no - 1) * limit}) pipeline.append({"$limit": limit}) - items = await SubmissionsModel.aggregate(pipeline).to_list() + items = await Submission.aggregate(pipeline).to_list() else: - items = await SubmissionsModel.aggregate(pipeline).to_list() + items = await Submission.aggregate(pipeline).to_list() total = len(items) return { "submissions": items, From 2ee489ae555f9a8373bc96c49b83098a3caeb02d Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 28 Jul 2025 10:53:10 -0400 Subject: [PATCH 07/16] Refactor and implement application class methods --- .../src/graphql/resolvers/form_resolvers.py | 37 +++- .../graphql/resolvers/submission_resolvers.py | 88 +++----- .../src/graphql/schema/__init__.py | 10 +- .../src/graphql/schema/form_schema.py | 55 +++-- .../src/graphql/schema/submission_schema.py | 60 +++--- .../src/graphql/service/form_service.py | 11 +- .../src/graphql/service/submission_service.py | 202 +----------------- .../src/models/formio/submission.py | 1 + .../src/models/webapi/application.py | 20 +- .../src/models/webapi/authorization.py | 1 + 10 files changed, 154 insertions(+), 331 deletions(-) diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py index f34613091e..77cbdeeffa 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -12,35 +12,62 @@ class QueryFormsResolver: @strawberry.field(extensions=[auth.auth_required()]) async def get_forms( self, - order_by: str = 'id', limit: int = 100, offset: int = 0, - type: Optional[str] = None + order_by: str = 'created', + type: Optional[str] = None, + created_by: Optional[str] = None, + form_name: Optional[str] = None, + status: Optional[str] = None, + parent_form_id: Optional[str] = None, + from_date: Optional[str] = None, + to_date: Optional[str] = None ) -> PaginationWindow[FormSchema]: """ GraphQL resolver for querying forms. Args: - order_by (str): Field to sort by (default: 'id') limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) - type (Optional[str]): Type of form to return + order_by (str): Filter to sort forms by (default: 'created') + type (Optional[str]): Filter on form type + created_by (Optional[str]): Filter on user who created the form + form_name (Optional[str]): Filter on form name + status (Optional[str]): Filter on form status + parent_form_id (Optional[str]): Filter on form parent id + from_date (Optional[str]): Filter from form date + to_date (Optional[str]): Filter to form date Returns: Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ filters = {} + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters["order_by"] = order_by if type: filters["type"] = type + if created_by: + filters["created_by"] = created_by + if form_name: + filters["form_name"] = form_name + if status: + filters["status"] = status + if parent_form_id: + filters["parent_form_id"] = parent_form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date forms = await FormService.get_forms( - order_by=order_by, limit=limit, offset=offset, filters=filters ) return forms + @strawberry.field(extensions=[auth.auth_required()]) async def get_form( self, diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index 0029f77b63..272d1a3c1a 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -3,92 +3,66 @@ import strawberry from strawberry.scalars import JSON -from src.graphql.schema import PaginationWindow, SubmissionSchema -from src.graphql.schema import PaginatedSubmissionResponse +from src.graphql.schema import ( + PaginationWindow, + SubmissionSchema, +) from src.graphql.service import SubmissionService from src.middlewares.auth import auth @strawberry.type class QuerySubmissionsResolver: - # @strawberry.field(extensions=[auth.auth_required()]) - @strawberry.field + @strawberry.field(extensions=[auth.auth_required()]) async def get_submissions( self, - order_by: str = 'id', limit: int = 100, offset: int = 0, + order_by: str = 'created', + application_status: Optional[str] = None, form_name: Optional[str] = None, + form_type: Optional[str] = None, + parent_form_id: Optional[str] = None, + from_date: Optional[str] = None, + to_date: Optional[str] = None ) -> PaginationWindow[SubmissionSchema]: """ GraphQL resolver for querying submissions. Args: - order_by (str): Field to sort by (default: 'id') limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) - form_name (Optional[str]): Name of the form to get submissions from + order_by (str): Filter to sort submissions by (default: 'created') + application_status (Optional[str]): Filter on submission status + form_name (Optional[str]): Filter on form name + form_type (Optional[str]): Filter on form type + parent_form_id (Optional[str]): Filter on form parent id + from_date (Optional[str]): Filter from submission date + to_date (Optional[str]): Filter to submission date Returns: Paginated list of Submission objects containing combined PostgreSQL and MongoDB data """ filters = {} + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters["order_by"] = order_by + if application_status: + filters["application_status"] = application_status if form_name: filters["form_name"] = form_name + if form_type: + filters["form_type"] = form_type + if parent_form_id: + filters["parent_form_id"] = parent_form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date forms = await SubmissionService.get_submissions( - order_by=order_by, limit=limit, offset=offset, filters=filters ) return forms - - # @strawberry.field(extensions=[auth.auth_required()]) - @strawberry.field - async def get_submission( - self, - info: strawberry.Info, - form_name: Optional[str] = None, - sort_by: str = "created", - sort_order: str = "desc", - parent_form_id: Optional[str] = None, - filters: Optional[JSON] = None, - selected_form_fields: Optional[List[str]] = None, - created_before: Optional[str] = None, - created_after: Optional[str] = None, - page_no: int = 1, - limit: int = 5, - ) -> Optional[PaginatedSubmissionResponse]: - """ - GraphQL resolver for querying submission. - - Args: - info (strawberry.Info): GraphQL context information - form_name (Optional[str]): Name of the form - sort_by (str): Field to sort by (default: "created") - sort_order (str): Order of sorting (default: "desc") - parent_form_id (Optional[str]): ID of the parent form - filters (Optional[JSON]): Filters to apply to the query - selected_form_fields (Optional[List[str]]): Form fields to include in the response - created_before (Optional[str]): Filter for submissions created before this date - created_after (Optional[str]): Filter for submissions created after this date - page_no (int): Page number for pagination (default: 1) - limit (int): Number of items per page (default: 5) - Returns: - Submission object containing combined SQL and MongoDB data - """ - submission = await SubmissionService.get_submission( - info=info, - form_name=form_name, - sort_by=sort_by, - sort_order=sort_order, - parent_form_id=parent_form_id, - filters=filters, - selected_form_fields=selected_form_fields, - created_before=created_before, - created_after=created_after, - page_no=page_no, - limit=limit, - ) - return submission diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 2d6fca4e77..a9361e682f 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -3,11 +3,7 @@ import strawberry from src.graphql.schema.form_schema import FormSchema -from src.graphql.schema.submission_schema import ( - PaginatedSubmissionResponse, - SubmissionDetailsWithSubmissionData, - SubmissionSchema, -) +from src.graphql.schema.submission_schema import SubmissionSchema Item = TypeVar("Item") @strawberry.type @@ -19,7 +15,5 @@ class PaginationWindow(Generic[Item]): __all__ = [ "FormSchema", "PaginationWindow", - "SubmissionSchema", - "SubmissionDetailsWithSubmissionData", - "PaginatedSubmissionResponse", + "SubmissionSchema" ] diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index 49dd8a2cbb..5653faa4e5 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -20,8 +20,8 @@ class FormSchema: type: str display: Optional[str] = None parent_form_id: Optional[str] = None - created_at: Optional[str] = None - modified_at: Optional[str] = None + created: Optional[str] = None + modified: Optional[str] = None # WebAPI populated fields created_by: str @@ -37,22 +37,35 @@ class FormSchema: @staticmethod def from_result(result: dict): - formio = result["formio"] - webapi = result["webapi"] - calculated = result["calculated"] - return FormSchema( - id=formio.id, - title=formio.title, - name=formio.name, - path=formio.path, - type=formio.type, - display=formio.display, - parent_form_id=formio.parentFormId, - created_at=(formio.created.isoformat() if formio.created else None), - modified_at=(formio.modified.isoformat() if formio.modified else None), - created_by=webapi.created_by, - modified_by=webapi.modified_by, - status=webapi.status, - version=webapi.version, - total_submissions=calculated["total_submissions"] - ) + data = {} + + # Map FormIO Data + if formio:= result.get("formio"): + data.update({ + "id": formio.id, + "title": formio.title, + "name": formio.name, + "path": formio.path, + "type": formio.type, + "display": formio.display, + "parent_form_id": formio.parentFormId, + "created": (formio.created.isoformat() if formio.created else None), + "modified": (formio.modified.isoformat() if formio.modified else None), + }) + + # Map WebAPI data + if webapi := result.get("webapi"): + data.update({ + "created_by": webapi.created_by, + "modified_by": webapi.modified_by, + "status": webapi.status, + "version": webapi.version, + }) + + # Map Calculated data + if calculated := result.get("calculated"): + data.update({ + "total_submissions": calculated["total_submissions"] + }) + + return FormSchema(**data) diff --git a/forms-flow-data-layer/src/graphql/schema/submission_schema.py b/forms-flow-data-layer/src/graphql/schema/submission_schema.py index 62766fe377..bd49675ca4 100644 --- a/forms-flow-data-layer/src/graphql/schema/submission_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/submission_schema.py @@ -36,39 +36,27 @@ class SubmissionSchema: @staticmethod def from_result(result: dict): - formio = result["formio"] - webapi = result["webapi"] - return SubmissionSchema( - id=webapi.id, - application_status=webapi.application_status, - form_id=webapi.latest_form_id, - submission_id=webapi.submission_id, - created_at=(webapi.created.isoformat() if webapi.created else None), - updated_at=(webapi.modified.isoformat() if webapi.modified else None), - created_by=webapi.created_by, - updated_by=webapi.modified_by, - is_resubmit=webapi.is_resubmit, - is_draft=webapi.is_draft, - data=formio.data - ) - - -@strawberry.type -class SubmissionDetailsWithSubmissionData: - id: int - created_by: str - submission_id: str - form_name: str - application_status: str - created: str - data: Optional[JSON] = ( - None # this data is the submission data from mongodb or we can pass any json data - ) - - -@strawberry.type -class PaginatedSubmissionResponse: - submissions: List[SubmissionDetailsWithSubmissionData] - total_count: int - page_no: Optional[int] = None - limit: Optional[int] = None + data = {} + + # Map WebAPI data + if webapi := result.get("webapi"): + data.update({ + "id": webapi.id, + "application_status": webapi.application_status, + "form_id": webapi.latest_form_id, + "submission_id": webapi.submission_id, + "created_at": (webapi.created.isoformat() if webapi.created else None), + "updated_at": (webapi.modified.isoformat() if webapi.modified else None), + "created_by": webapi.created_by, + "updated_by": webapi.modified_by, + "is_resubmit": webapi.is_resubmit, + "is_draft": webapi.is_draft + }) + + # Map FormIO data + if formio := result.get("formio"): + data.update({ + "data": formio.data + }) + + return SubmissionSchema(**data) if data else None diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 65b0cd1bfb..7902cb3ecb 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -17,7 +17,6 @@ class FormService(BaseService): @classmethod async def get_forms( cls, - order_by: str, limit: int = 100, offset: int = 0, filters: dict[str, str] = {}, @@ -26,7 +25,6 @@ async def get_forms( Fetches forms from the WebAPI and adds additional details from FormIO. Args: - order_by (str): Field to sort by (default: 'id') limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) filters (dict): Search filters to apply to the query @@ -49,12 +47,11 @@ async def get_forms( # Convert to GraphQL Schema forms = [FormSchema.from_result(result=r) for r in results] - forms.sort(key=lambda x: getattr(x, order_by)) return PaginationWindow(items=forms, total_count=webapi_total_count) - @staticmethod - async def get_form(form_id: str) -> Optional[FormSchema]: + @classmethod + async def get_form(cls, form_id: str) -> Optional[FormSchema]: """ Fetches a form based on it's form_id from the WebAPI and adds additional details from FormIO. @@ -66,11 +63,13 @@ async def get_form(form_id: str) -> Optional[FormSchema]: # Query the databases webapi_result = await FormProcessMapper.first(form_id=form_id) formio_result = await Form.get(PydanticObjectId(form_id)) + _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(webapi_result.form_id)}) # Combine results result = { "webapi": webapi_result, - "formio": formio_result + "formio": formio_result, + "calculated": {"total_submissions": submissions_count} } # Convert to GraphQL Schema diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index a1d3210ddc..fab9ffe659 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -1,13 +1,9 @@ from typing import Any, Dict, List, Optional, Tuple -import strawberry from beanie import PydanticObjectId from src.graphql.schema import PaginationWindow, SubmissionSchema -from src.graphql.schema import ( - PaginatedSubmissionResponse, - SubmissionDetailsWithSubmissionData, -) +from src.graphql.service import BaseService from src.models.formio import Submission from src.models.webapi import Application from src.utils import get_logger @@ -15,13 +11,12 @@ logger = get_logger(__name__) -class SubmissionService: +class SubmissionService(BaseService): """Service class for handling submission related operations on mongo and webapi side.""" @classmethod async def get_submissions( cls, - order_by: str, limit: int = 100, offset: int = 0, filters: dict[str, str] = {}, @@ -30,7 +25,6 @@ async def get_submissions( Fetches submissions from the WebAPI and adds additional details from FormIO. Args: - order_by (str): Field to sort by (default: 'id') limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) filters (dict): Search filters to apply to the query @@ -44,197 +38,11 @@ async def get_submissions( results = [] webapi_results = webapi_query.all() for wr in webapi_results: - _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(wr.form_id)}) results.append({ "webapi": wr, - "formio": await Submission.get(PydanticObjectId(wr.form_id)), - "calculated": {"total_submissions": submissions_count} + "formio": await Submission.get(PydanticObjectId(wr.submission_id)), }) # Convert to GraphQL Schema - forms = [SubmissionSchema.from_result(result=r) for r in results] - forms.sort(key=lambda x: getattr(x, order_by)) - return PaginationWindow(items=forms, total_count=webapi_total_count) - - @staticmethod - async def split_search_criteria( - search: Optional[Dict[str, Any]], webapi_fields: List[str] - ) -> Tuple[Dict[str, Any], Dict[str, Any]]: - """Splits search criteria into webapi and mongo search dictionaries.""" - webapi_search = {} - mongo_search = {} - - if search: - for field, value in search.items(): - if field in webapi_fields: - webapi_search[field] = value - else: - mongo_search[field] = value - - return webapi_search, mongo_search - - @staticmethod - def _process_results( - webapi_side_submissions, mongo_side_submissions, is_sort_on_webapi_side, limit - ): - """Process results and merge with MongoDB data.""" - - # Pre-process MongoDB submissions once (convert _id to submission_id and create lookup dict) - mongo_dict = {} - lookup_key = "submission_id" - for sub in mongo_side_submissions: - submission_id = sub.pop( - "_id" - ) # Remove the _id field from the submission data - mongo_dict[submission_id] = sub - - # Create a single processing path regardless of sort origin - if is_sort_on_webapi_side: - source_data = webapi_side_submissions - lookup_dict = mongo_dict - else: - source_data = [ - {"submission_id": sid} for sid in mongo_dict.keys() - ] # instead of full data just mock the data by giving the value as submission_id - lookup_dict = { - app["submission_id"]: app - for app in webapi_side_submissions - if app["submission_id"] - } - - # Single merging logic - final_results = [] - for item in source_data: - submission_id = item[lookup_key] - if submission_id in lookup_dict: - # the variable item and lookup_dict[submission_id] are same in the case of webapi side sort - base_data = ( - item if is_sort_on_webapi_side else lookup_dict[submission_id] - ) - final_results.append( - {**base_data, "submission_data": mongo_dict[submission_id]} - ) - # Apply limit if provided - if limit and len(final_results) >= limit: - break - - return final_results - - @staticmethod - async def get_submission( - info: strawberry.Info, - form_name: str, - sort_by: str, - sort_order: str, - parent_form_id: str, - filters: Dict, - selected_form_fields: List[str], - created_before: Optional[str], - created_after: Optional[str], - page_no: int, - limit: int, - ) -> Optional[PaginatedSubmissionResponse]: - """ - Fetches submissions from both webapi and MongoDB, merges them, and returns a paginated response. - Args: form_name: Name of the form - sort_by: Field to sort by (default: "created") - sort_order: Order of sorting (default: "desc") - parent_form_id: ID of the parent form - filters: Filters to apply to the query - selected_form_fields: Fields to select from MongoDB - page_no: Page number for pagination - limit: Number of records per page - Returns: - PaginatedSubmissionResponse: A paginated response containing submissions - and total count. - """ - # Get user context from token - user = info.context["user"] - tenant_key = user.tenant_key - user_groups = user.token_info.get("groups", []) - webapi_fields = [ - "created_by", - "application_status", - "id", - "created", - "form_name", - ] - # drived filter mongo serach and webapi search - webapi_search, mongo_search = await SubmissionService.split_search_criteria( - filters, webapi_fields - ) - logger.info( - f"extracted filter by mongo {mongo_search} and webapi {webapi_search}" - ) - - is_paginate_on_webapi_side = not mongo_search - is_sort_on_webapi_side = sort_by in webapi_fields - sort_params = {"sort_by": sort_by, "sort_order": sort_order} - webapi_side_submissions, total_count = ( - await Application.get_authorized_applications( - tenant_key=tenant_key, - roles=user_groups, - is_paginate=is_paginate_on_webapi_side, - form_name=form_name, - filter=webapi_search, - created_before=created_before, - created_after=created_after, - page_no=page_no, - limit=limit, - parent_form_id=parent_form_id, - **(sort_params if is_sort_on_webapi_side else {}), - ) - ) - - mongo_side_submissions = {} - final_out_puts = None - needs_mongo_submissions = ( - webapi_side_submissions and parent_form_id - ) # if parent_form_id and webapi side submission is non empty then only go to mongo side - if needs_mongo_submissions: - logger.info("Fetching submission data from formio.") - # Extract the submission IDs - submission_ids = [ - app["submission_id"] - for app in webapi_side_submissions - if app["submission_id"] - ] - # Get filtered submissions from MongoDB - mongo_side_submissions = await Submission.query_submission( - submission_ids=submission_ids, - filter=mongo_search, - selected_form_fields=selected_form_fields, - page_no=not is_paginate_on_webapi_side and page_no or None, - limit=not is_paginate_on_webapi_side and limit or None, - **(sort_params if not is_sort_on_webapi_side else {}), - ) - final_out_puts = SubmissionService._process_results( - webapi_side_submissions, - mongo_side_submissions.get("submissions", []), - is_sort_on_webapi_side, - limit, - ) - # sometimes webapi_side_submission will be no empty but mongo side submission will be empty - data = final_out_puts if needs_mongo_submissions else webapi_side_submissions - return PaginatedSubmissionResponse( - submissions=[ - SubmissionDetailsWithSubmissionData( - id=row.get("id"), - form_name=row.get("form_name"), - submission_id=row.get("submission_id"), - created_by=row.get("created_by"), - application_status=row.get("application_status"), - created=row.get("created"), - data=row.get("submission_data", {}), - ) - for row in data - ], - total_count=( - mongo_side_submissions.get("total_count", 0) - if mongo_search - and needs_mongo_submissions - else total_count - ), - page_no=page_no, - limit=limit, - ) + submissions = [SubmissionSchema.from_result(result=r) for r in results] + return PaginationWindow(items=submissions, total_count=webapi_total_count) diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index 3eaeb18045..3b5f491f54 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -8,6 +8,7 @@ class Submission(Document): data: dict + form: PydanticObjectId class Settings: name = FormioTables.SUBMISSIONS.value diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 61cbd22fac..454a692746 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -19,6 +19,25 @@ class Application(BaseModel): _table_name = WebApiTables.APPLICATION.value _table = None + @classmethod + async def first(cls, **filters): + return await super().first(**filters) + + @classmethod + async def find_all(cls, **filters): + query = await super().find_all(**filters) + table = await cls.get_table() + + # Apply date filters, if any + if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): + query = query.order_by(order_by) + if from_date := filters.get("from_date"): + query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) + if to_date := filters.get("to_date"): + query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) + + return query + @classmethod def filter_query(cls, query, filter_data: dict, application_table, mapper_table): """Apply filters to the SQLAlchemy query.""" @@ -36,7 +55,6 @@ def filter_query(cls, query, filter_data: dict, application_table, mapper_table) else: # For other fields, use ilike for case-insensitive search query = query.where(col.ilike(f"%{value}%")) - return query @classmethod def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): diff --git a/forms-flow-data-layer/src/models/webapi/authorization.py b/forms-flow-data-layer/src/models/webapi/authorization.py index 1a222741f6..681f471d8a 100644 --- a/forms-flow-data-layer/src/models/webapi/authorization.py +++ b/forms-flow-data-layer/src/models/webapi/authorization.py @@ -7,6 +7,7 @@ from .base import BaseModel from .constants import WebApiTables + class AuthType(Enum): APPLICATION = "APPLICATION" FORM = "FORM" From 22792ec5eff5625944d98fabae80a08e279ccc15 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 28 Jul 2025 11:28:44 -0400 Subject: [PATCH 08/16] Implement get_submission and remove base service --- .../src/graphql/service/__init__.py | 3 +- .../src/graphql/service/base_service.py | 42 ------------------- .../src/graphql/service/form_service.py | 14 ++++--- .../src/graphql/service/submission_service.py | 36 ++++++++++++++-- .../src/models/formio/submission.py | 12 ++++++ .../src/models/webapi/base.py | 13 ++++-- 6 files changed, 63 insertions(+), 57 deletions(-) delete mode 100644 forms-flow-data-layer/src/graphql/service/base_service.py diff --git a/forms-flow-data-layer/src/graphql/service/__init__.py b/forms-flow-data-layer/src/graphql/service/__init__.py index fb1a9288c3..39470e71f4 100644 --- a/forms-flow-data-layer/src/graphql/service/__init__.py +++ b/forms-flow-data-layer/src/graphql/service/__init__.py @@ -1,5 +1,4 @@ -from src.graphql.service.base_service import BaseService from src.graphql.service.form_service import FormService from src.graphql.service.submission_service import SubmissionService -__all__ = ["BaseService", "FormService", "SubmissionService"] +__all__ = ["FormService", "SubmissionService"] diff --git a/forms-flow-data-layer/src/graphql/service/base_service.py b/forms-flow-data-layer/src/graphql/service/base_service.py deleted file mode 100644 index ae4a4b0035..0000000000 --- a/forms-flow-data-layer/src/graphql/service/base_service.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Generic, List, TypeVar - -from src.utils import get_logger - -logger = get_logger(__name__) -MongoModel = TypeVar("MongoModel") -PostgresModel = TypeVar("PostgresModel") - - -class BaseService(): - @staticmethod - async def _formio_find_all( - model: Generic[MongoModel], - limit: int = None, - offset: int = None, - filters: dict = None - ) -> List[MongoModel]: - query = model.find_all() - - # Apply filters - for filter, value in filters.items(): - if hasattr(model, filter): - query = query.find(getattr(model, filter) == value) - total_count = await query.count() - - # Apply pagination - results = query.skip(offset).limit(limit) - return results, total_count - - @staticmethod - async def _webapi_find_all( - model: Generic[PostgresModel], - limit: int = None, - offset: int = None, - filters: dict = None - ) -> List[PostgresModel]: - query = await model.find_all(**filters) - total_count = len((await model.execute(query)).all()) - - # Apply pagination - results = (await model.execute(query.offset(offset).limit(limit))) - return results, total_count diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 7902cb3ecb..0e308940d9 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -3,7 +3,6 @@ from beanie import PydanticObjectId from src.graphql.schema import FormSchema, PaginationWindow -from src.graphql.service import BaseService from src.models.formio import Form, Submission from src.models.webapi import FormProcessMapper from src.utils import get_logger @@ -11,7 +10,7 @@ logger = get_logger(__name__) -class FormService(BaseService): +class FormService(): """Service class for handling form related operations.""" @classmethod @@ -32,13 +31,16 @@ async def get_forms( Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ # Query webapi database - webapi_query, webapi_total_count = await cls._webapi_find_all(FormProcessMapper, limit, offset, filters) + webapi_query, webapi_total_count = await FormProcessMapper.find_all(**filters) + + # Apply pagination filters + webapi_query = webapi_query.offset(offset).limit(limit) # Combine results with data from formio results = [] - webapi_results = webapi_query.all() + webapi_results = (await FormProcessMapper.execute(webapi_query)).all() for wr in webapi_results: - _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(wr.form_id)}) + submissions_count = await Submission.count(filters={"form": PydanticObjectId(wr.form_id)}) results.append({ "webapi": wr, "formio": await Form.get(PydanticObjectId(wr.form_id)), @@ -63,7 +65,7 @@ async def get_form(cls, form_id: str) -> Optional[FormSchema]: # Query the databases webapi_result = await FormProcessMapper.first(form_id=form_id) formio_result = await Form.get(PydanticObjectId(form_id)) - _, submissions_count = await cls._formio_find_all(Submission, filters={"form": PydanticObjectId(webapi_result.form_id)}) + submissions_count = await Submission.count(filters={"form": PydanticObjectId(webapi_result.form_id)}) # Combine results result = { diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index fab9ffe659..d85b853599 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -3,7 +3,6 @@ from beanie import PydanticObjectId from src.graphql.schema import PaginationWindow, SubmissionSchema -from src.graphql.service import BaseService from src.models.formio import Submission from src.models.webapi import Application from src.utils import get_logger @@ -11,7 +10,7 @@ logger = get_logger(__name__) -class SubmissionService(BaseService): +class SubmissionService(): """Service class for handling submission related operations on mongo and webapi side.""" @classmethod @@ -32,11 +31,14 @@ async def get_submissions( Paginated list of Submission objects containing combined PostgreSQL and MongoDB data """ # Query webapi database - webapi_query, webapi_total_count = await cls._webapi_find_all(Application, limit, offset, filters) + webapi_query, webapi_total_count = Application.find_all(filters) + + # Apply pagination filters + webapi_query = webapi_query.offset(offset).limit(limit) # Combine results with data from formio results = [] - webapi_results = webapi_query.all() + webapi_results = (await Application.execute(webapi_query)).all() for wr in webapi_results: results.append({ "webapi": wr, @@ -46,3 +48,29 @@ async def get_submissions( # Convert to GraphQL Schema submissions = [SubmissionSchema.from_result(result=r) for r in results] return PaginationWindow(items=submissions, total_count=webapi_total_count) + + + @classmethod + async def get_submission(cls, submission_id: str) -> Optional[SubmissionSchema]: + """ + Fetches a submission based on it's submission_id from the WebAPI and adds additional details from FormIO. + + Args: + submission_id (str): ID of the submission + Returns: + Submission object containing combined PostgreSQL and MongoDB data + """ + # Query the databases + webapi_result = await Application.first(submission_id=submission_id) + formio_result = await Submission.get(PydanticObjectId(submission_id)) + + # Combine results + result = { + "webapi": webapi_result, + "formio": formio_result, + } + + # Convert to GraphQL Schema + submission = SubmissionSchema.from_result(result=result) + return submission + diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index 3b5f491f54..ea4423c286 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -13,6 +13,18 @@ class Submission(Document): class Settings: name = FormioTables.SUBMISSIONS.value + @classmethod + async def count(cls, **filters): + """Count number of entries that match the passed filters.""" + query = cls.find_all() + + # Apply filters + for filter, value in filters.items(): + if hasattr(cls, filter): + query = query.find(getattr(cls, filter) == value) + + return (await query.count()) + @staticmethod def _build_match_stage(submission_ids: List[str], filter: Optional[dict]) -> dict: """Build the MongoDB match stage.""" diff --git a/forms-flow-data-layer/src/models/webapi/base.py b/forms-flow-data-layer/src/models/webapi/base.py index 04eda77cab..4e71f4ff37 100644 --- a/forms-flow-data-layer/src/models/webapi/base.py +++ b/forms-flow-data-layer/src/models/webapi/base.py @@ -1,4 +1,5 @@ from sqlalchemy import select +from sqlalchemy.sql import func from src.db.webapi_db import webapi_db @@ -29,16 +30,22 @@ async def get_table(cls): if cls._table is None: cls._table = await webapi_db.get_table(cls._table_name) return cls._table - + + @classmethod + async def count(cls, **filters): + """Count number of entries that match the passed filters.""" + stmt = await cls.find_all(**filters) + return (await cls.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one() + @classmethod async def first(cls, **filters): - """Find the first entries that match the passed args.""" + """Find the first entries that match the passed filters.""" stmt = await cls.find_all(**filters) return (await cls.execute(stmt)).first() @classmethod async def find_all(cls, **filters): - """Find all entries that match the passed args.""" + """Find all entries that match the passed filters.""" table = await cls.get_table() stmt = select(table) for key, value in filters.items(): From 20ae99fd2bc6cbfee16d9c6ef71d58153dbe021a Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 28 Jul 2025 15:00:27 -0400 Subject: [PATCH 09/16] Implement get_submission query --- .../src/graphql/resolvers/form_resolvers.py | 9 ++- .../graphql/resolvers/submission_resolvers.py | 31 +++++++- .../src/graphql/schema/__init__.py | 12 +-- .../src/graphql/schema/form_schema.py | 2 +- .../src/graphql/schema/submission_schema.py | 3 +- .../src/graphql/service/form_service.py | 18 ++++- .../src/graphql/service/submission_service.py | 18 ++++- .../src/middlewares/pagination.py | 24 ++++++ .../src/models/formio/form.py | 9 +++ .../src/models/formio/submission.py | 78 ------------------- .../src/models/webapi/application.py | 33 +------- .../src/models/webapi/authorization.py | 2 - .../src/models/webapi/form_process_mapper.py | 21 ++++- 13 files changed, 120 insertions(+), 140 deletions(-) create mode 100644 forms-flow-data-layer/src/middlewares/pagination.py diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py index 77cbdeeffa..c3584e225c 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -12,6 +12,7 @@ class QueryFormsResolver: @strawberry.field(extensions=[auth.auth_required()]) async def get_forms( self, + info: strawberry.Info, limit: int = 100, offset: int = 0, order_by: str = 'created', @@ -27,6 +28,7 @@ async def get_forms( GraphQL resolver for querying forms. Args: + info (strawberry.Info): GraphQL context information limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) order_by (str): Filter to sort forms by (default: 'created') @@ -40,10 +42,9 @@ async def get_forms( Returns: Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ - filters = {} - # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names # will be applied automatically. Other filters will require additional handling. + filters = {} filters["order_by"] = order_by if type: filters["type"] = type @@ -61,6 +62,7 @@ async def get_forms( filters["to_date"] = to_date forms = await FormService.get_forms( + user_context=info.context.get("user"), limit=limit, offset=offset, filters=filters @@ -71,17 +73,20 @@ async def get_forms( @strawberry.field(extensions=[auth.auth_required()]) async def get_form( self, + info: strawberry.Info, form_id: str, ) -> Optional[FormSchema]: """ GraphQL resolver for querying form. Args: + info (strawberry.Info): GraphQL context information form_id (str): ID of the form Returns: Form object containing combined PostgreSQL and MongoDB data """ form = await FormService.get_form( + user_context=info.context.get("user"), form_id=form_id, ) return form diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index 272d1a3c1a..bc962973f8 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -1,7 +1,6 @@ -from typing import List, Optional +from typing import Optional import strawberry -from strawberry.scalars import JSON from src.graphql.schema import ( PaginationWindow, @@ -16,6 +15,7 @@ class QuerySubmissionsResolver: @strawberry.field(extensions=[auth.auth_required()]) async def get_submissions( self, + info: strawberry.Info, limit: int = 100, offset: int = 0, order_by: str = 'created', @@ -30,6 +30,7 @@ async def get_submissions( GraphQL resolver for querying submissions. Args: + info (strawberry.Info): GraphQL context information limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) order_by (str): Filter to sort submissions by (default: 'created') @@ -42,10 +43,9 @@ async def get_submissions( Returns: Paginated list of Submission objects containing combined PostgreSQL and MongoDB data """ - filters = {} - # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names # will be applied automatically. Other filters will require additional handling. + filters = {} filters["order_by"] = order_by if application_status: filters["application_status"] = application_status @@ -61,8 +61,31 @@ async def get_submissions( filters["to_date"] = to_date forms = await SubmissionService.get_submissions( + user_context=info.context.get("user"), limit=limit, offset=offset, filters=filters ) return forms + + + @strawberry.field(extensions=[auth.auth_required()]) + async def get_submission( + self, + info: strawberry.Info, + submission_id: str, + ) -> Optional[SubmissionSchema]: + """ + GraphQL resolver for querying submission. + + Args: + info (strawberry.Info): GraphQL context information + submission_id (str): ID of the submission + Returns: + Submission object containing combined PostgreSQL and MongoDB data + """ + submission = await SubmissionService.get_submission( + user_context=info.context.get("user"), + submission_id=submission_id, + ) + return submission diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index a9361e682f..3dabd19808 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,16 +1,6 @@ -from typing import Generic, List, TypeVar - -import strawberry - from src.graphql.schema.form_schema import FormSchema from src.graphql.schema.submission_schema import SubmissionSchema - -Item = TypeVar("Item") -@strawberry.type -class PaginationWindow(Generic[Item]): - """GraphQL type representing a generic set of paginated items.""" - items: List[Item] - total_count: int +from src.middlewares.pagination import PaginationWindow __all__ = [ "FormSchema", diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index 5653faa4e5..f8a346cf57 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Optional import strawberry diff --git a/forms-flow-data-layer/src/graphql/schema/submission_schema.py b/forms-flow-data-layer/src/graphql/schema/submission_schema.py index bd49675ca4..82410df9ac 100644 --- a/forms-flow-data-layer/src/graphql/schema/submission_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/submission_schema.py @@ -1,9 +1,8 @@ """Managing webapi schemas.""" -from typing import List, Optional +from typing import Optional import strawberry -from strawberry.scalars import JSON @strawberry.type diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 0e308940d9..563e813c05 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -1,11 +1,12 @@ -from typing import List, Optional +from typing import Optional from beanie import PydanticObjectId from src.graphql.schema import FormSchema, PaginationWindow +from src.middlewares.pagination import verify_pagination_params from src.models.formio import Form, Submission from src.models.webapi import FormProcessMapper -from src.utils import get_logger +from src.utils import UserContext, get_logger logger = get_logger(__name__) @@ -14,8 +15,10 @@ class FormService(): """Service class for handling form related operations.""" @classmethod + @verify_pagination_params async def get_forms( cls, + user_context: UserContext, limit: int = 100, offset: int = 0, filters: dict[str, str] = {}, @@ -24,6 +27,7 @@ async def get_forms( Fetches forms from the WebAPI and adds additional details from FormIO. Args: + user_context (UserContext): User context information limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) filters (dict): Search filters to apply to the query @@ -31,7 +35,8 @@ async def get_forms( Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ # Query webapi database - webapi_query, webapi_total_count = await FormProcessMapper.find_all(**filters) + webapi_query = await FormProcessMapper.find_all(**filters) + webapi_total_count = await FormProcessMapper.count(**filters) # Apply pagination filters webapi_query = webapi_query.offset(offset).limit(limit) @@ -53,11 +58,16 @@ async def get_forms( @classmethod - async def get_form(cls, form_id: str) -> Optional[FormSchema]: + async def get_form( + cls, + user_context: UserContext, + form_id: str + ) -> Optional[FormSchema]: """ Fetches a form based on it's form_id from the WebAPI and adds additional details from FormIO. Args: + user_context (UserContext): User context information form_id (str): ID of the form Returns: Form object containing combined PostgreSQL and MongoDB data diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index d85b853599..521a916d30 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -1,11 +1,12 @@ -from typing import Any, Dict, List, Optional, Tuple +from typing import Optional from beanie import PydanticObjectId from src.graphql.schema import PaginationWindow, SubmissionSchema +from src.middlewares.pagination import verify_pagination_params from src.models.formio import Submission from src.models.webapi import Application -from src.utils import get_logger +from src.utils import UserContext, get_logger logger = get_logger(__name__) @@ -14,8 +15,10 @@ class SubmissionService(): """Service class for handling submission related operations on mongo and webapi side.""" @classmethod + @verify_pagination_params async def get_submissions( cls, + user_context: UserContext, limit: int = 100, offset: int = 0, filters: dict[str, str] = {}, @@ -24,6 +27,7 @@ async def get_submissions( Fetches submissions from the WebAPI and adds additional details from FormIO. Args: + user_context (UserContext): User context information limit (int): Number of items to return (default: 100) offset (int): Pagination offset (default: 0) filters (dict): Search filters to apply to the query @@ -31,7 +35,8 @@ async def get_submissions( Paginated list of Submission objects containing combined PostgreSQL and MongoDB data """ # Query webapi database - webapi_query, webapi_total_count = Application.find_all(filters) + webapi_query = await Application.find_all(**filters) + webapi_total_count =await Application.count(**filters) # Apply pagination filters webapi_query = webapi_query.offset(offset).limit(limit) @@ -51,11 +56,16 @@ async def get_submissions( @classmethod - async def get_submission(cls, submission_id: str) -> Optional[SubmissionSchema]: + async def get_submission( + cls, + user_context: UserContext, + submission_id: str + ) -> Optional[SubmissionSchema]: """ Fetches a submission based on it's submission_id from the WebAPI and adds additional details from FormIO. Args: + user_context (UserContext): User context information submission_id (str): ID of the submission Returns: Submission object containing combined PostgreSQL and MongoDB data diff --git a/forms-flow-data-layer/src/middlewares/pagination.py b/forms-flow-data-layer/src/middlewares/pagination.py new file mode 100644 index 0000000000..dc874bbe97 --- /dev/null +++ b/forms-flow-data-layer/src/middlewares/pagination.py @@ -0,0 +1,24 @@ +import functools +from typing import Generic, List, TypeVar + +import strawberry + +Item = TypeVar("Item") +@strawberry.type +class PaginationWindow(Generic[Item]): + """GraphQL type representing a generic set of paginated items.""" + items: List[Item] + total_count: int + + +def verify_pagination_params(function): + """Verifies pagination parameters are valid.""" + + @functools.wraps(function) + def wrapper(*args, **kwargs): + if (limit := kwargs.get('limit')) and limit < 0: + raise Exception(f"limit ({limit}) must be greater than 0") + if (offset := kwargs.get('offset')) and offset < 0: + raise Exception(f"offset ({offset}) must be greater than 0") + return function(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/forms-flow-data-layer/src/models/formio/form.py b/forms-flow-data-layer/src/models/formio/form.py index e472a200df..7e5a2f308a 100644 --- a/forms-flow-data-layer/src/models/formio/form.py +++ b/forms-flow-data-layer/src/models/formio/form.py @@ -19,3 +19,12 @@ class Form(Document): class Settings: name = FormioTables.FORMS.value + + @classmethod + async def count(cls, **filters): + """Count number of entries that match the passed filters.""" + query = cls.find_all() + for filter, value in filters.items(): + if hasattr(cls, filter): + query = query.find(getattr(cls, filter) == value) + return (await query.count()) diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index ea4423c286..6db69f4649 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -17,85 +17,7 @@ class Settings: async def count(cls, **filters): """Count number of entries that match the passed filters.""" query = cls.find_all() - - # Apply filters for filter, value in filters.items(): if hasattr(cls, filter): query = query.find(getattr(cls, filter) == value) - return (await query.count()) - - @staticmethod - def _build_match_stage(submission_ids: List[str], filter: Optional[dict]) -> dict: - """Build the MongoDB match stage.""" - match_stage = {} - if submission_ids: - match_stage["_id"] = {"$in": [ObjectId(id) for id in submission_ids]} - if filter: - for field, value in filter.items(): - if isinstance(value, str): - # Only use regex for string values - match_stage[f"data.{field}"] = {"$regex": value, "$options": "i"} - else: - # For non-string values (numbers, booleans, etc.), use exact match - match_stage[f"data.{field}"] = value - return match_stage - - @staticmethod - def _build_sort_stage(sort_by: str, sort_order: str) -> Optional[dict]: - """Build the MongoDB sort stage if needed.""" - if not sort_by: - return None - sort_field = f"data.{sort_by}" - sort_value = 1 if sort_order.lower() == "asc" else -1 - return {"$sort": {sort_field: sort_value}} - - @staticmethod - def _build_projection_stage(project_fields: Optional[List[str]]) -> dict: - """Build the MongoDB projection stage.""" - project_stage = {"$project": { - "_id": {"$toString": "$_id"}, - }} - if project_fields: - for field in project_fields: - project_stage["$project"][field] = f"$data.{field}" - return project_stage - - @staticmethod - async def query_submission( - submission_ids: List[str], - filter: Optional[dict] = None, - selected_form_fields: Optional[List[str]] = None, - page_no: Optional[int] = None, - limit: Optional[int] = None, - sort_by: Optional[str] = None, - sort_order: str = "asc", - ) -> Dict[str, Any]: - """ - Query submissions from MongoDB with optional pagination and sorting. - """ - # Build match stage - match_stage = Submission._build_match_stage( - submission_ids=submission_ids, filter=filter - ) - pipeline = [{"$match": match_stage}] - - # Add sorting if sort_by is specified - if sort_stage := Submission._build_sort_stage(sort_by, sort_order): - pipeline.append(sort_stage) - - # Projection stage - pipeline.append(Submission._build_projection_stage(selected_form_fields)) - # Only add pagination if page_no and limit specified - if page_no is not None and limit is not None: - total = await Submission.aggregate(pipeline).count() - pipeline.append({"$skip": (page_no - 1) * limit}) - pipeline.append({"$limit": limit}) - items = await Submission.aggregate(pipeline).to_list() - else: - items = await Submission.aggregate(pipeline).to_list() - total = len(items) - return { - "submissions": items, - "total_count": total, - } diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 454a692746..fd0946d26d 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -1,8 +1,6 @@ from datetime import datetime -from sqlalchemy import and_, desc, or_, select -from sqlalchemy.sql import func -from src.db.webapi_db import webapi_db +from sqlalchemy import and_, or_, select from .authorization import Authorization, AuthType from .base import BaseModel @@ -72,16 +70,6 @@ async def get_authorized_applications( cls, tenant_key: str, roles: list[str], - form_name: str, - parent_form_id: str, - filter: dict = None, - created_before: str = None, - created_after: str = None, - sort_by: str = None, - sort_order: str = None, - is_paginate: bool = False, - page_no: int = 1, - limit: int = 5, ): """ Fetches authorized applications based on the provided parameters. @@ -89,14 +77,6 @@ async def get_authorized_applications( application_table = await cls.get_table() mapper_table = await FormProcessMapper.get_table() authorization_table = await Authorization.get_table() - # ["created_by", "application_status", "id", "created", "form_name"] - sortable_fields = { - "application_status": application_table.c.application_status, - "id": application_table.c.id, - "created_by": application_table.c.created_by, - "created": application_table.c.created, - "form_name": mapper_table.c.form_name, - } query = select( application_table, mapper_table.c.parent_form_id, mapper_table.c.form_name @@ -107,21 +87,12 @@ async def get_authorized_applications( authorization_table=authorization_table, roles=roles ) - # Optional conditions - form_name_condition = ( - mapper_table.c.form_name == form_name if form_name else True - ) - parent_form_id_condition = ( - mapper_table.c.parent_form_id == parent_form_id if parent_form_id else True - ) # Join tables query = query.join( mapper_table, and_( application_table.c.form_process_mapper_id == mapper_table.c.id, - form_name_condition, # Ensure form name matches if provided - parent_form_id_condition, # Ensure parent form ID matches if provided mapper_table.c.tenant == tenant_key, # Ensure tenant key matches in both tables ), @@ -170,6 +141,6 @@ async def get_authorized_applications( result = await cls.execute(query) applications = result.mappings().all() - total_count = len(applications) if not is_paginate else total_count + total_count = len(applications) return applications, total_count diff --git a/forms-flow-data-layer/src/models/webapi/authorization.py b/forms-flow-data-layer/src/models/webapi/authorization.py index 681f471d8a..4e581ddddd 100644 --- a/forms-flow-data-layer/src/models/webapi/authorization.py +++ b/forms-flow-data-layer/src/models/webapi/authorization.py @@ -2,8 +2,6 @@ from sqlalchemy import and_, or_, select -from src.db.webapi_db import webapi_db - from .base import BaseModel from .constants import WebApiTables diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py index c01a40e480..700a0b7887 100644 --- a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -1,8 +1,9 @@ -from src.db.webapi_db import webapi_db +from datetime import datetime from .base import BaseModel from .constants import WebApiTables + class FormProcessMapper(BaseModel): """ FormProcessMapper class to handle mapper-related information. @@ -11,3 +12,21 @@ class FormProcessMapper(BaseModel): _table_name = WebApiTables.FORM_PROCESS_MAPPER.value _table = None # cache for the mapper table + @classmethod + async def first(cls, **filters): + return await super().first(**filters) + + @classmethod + async def find_all(cls, **filters): + query = await super().find_all(**filters) + table = await cls.get_table() + + # Apply date filters, if any + if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): + query = query.order_by(order_by) + if from_date := filters.get("from_date"): + query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) + if to_date := filters.get("to_date"): + query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) + + return query \ No newline at end of file From 6189594a9abff6ad5c6ced3f73c9ea4147aaa8ab Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 28 Jul 2025 15:03:04 -0400 Subject: [PATCH 10/16] Fix typo --- .../src/components/Dashboard/ApplicationCounter.js | 4 ++-- forms-flow-web/src/components/Dashboard/Dashboard.js | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js index de01fcd2ef..161d9c1149 100644 --- a/forms-flow-web/src/components/Dashboard/ApplicationCounter.js +++ b/forms-flow-web/src/components/Dashboard/ApplicationCounter.js @@ -9,7 +9,7 @@ const ApplicationCounter = React.memo((props) => { application, getStatusDetails, noOfApplicationsAvailable, - setSHowSubmissionData, + setShowSubmissionData, } = props; if (noOfApplicationsAvailable === 0) { return ( @@ -28,7 +28,7 @@ const ApplicationCounter = React.memo((props) => {
{ - setSHowSubmissionData(app); + setShowSubmissionData(app); }} key={idx} > diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index 2c938e3d72..f47c9da7ac 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -72,7 +72,7 @@ const Dashboard = React.memo(() => { const [searchBy, setSearchBy] = useState("created"); const [sortsBy, setSortsBy] = useState("formName"); - const [showSubmissionData, setSHowSubmissionData] = useState(submissionsList[0]); + const [showSubmissionData, setShowSubmissionData] = useState(submissionsList[0]); const [show, setShow] = useState(false); // State to set search text for submission data //Array for pagination dropdown @@ -134,7 +134,7 @@ const Dashboard = React.memo(() => { }, [dispatch, activePage, limit, sortsBy, sortOrder, dateRange, searchText, searchBy]); useEffect(() => { - setSHowSubmissionData(submissionsList[0]); + setShowSubmissionData(submissionsList[0]); }, [submissionsList]); const onChangeInput = (option) => { @@ -272,7 +272,7 @@ const Dashboard = React.memo(() => { application={submissionsList} getStatusDetails={getStatusDetails} noOfApplicationsAvailable={noOfApplicationsAvailable} - setSHowSubmissionData={setSHowSubmissionData} + setShowSubmissionData={setShowSubmissionData} /> )}
@@ -353,10 +353,8 @@ const Dashboard = React.memo(() => {

{t("Submission Status")}

-
setShow(false)} aria-label="Close"> - -
- + + Date: Tue, 29 Jul 2025 09:25:07 -0400 Subject: [PATCH 11/16] Add metric endpoint --- .../src/graphql/resolvers/__init__.py | 3 +- .../src/graphql/resolvers/metric_resolvers.py | 48 +++++++++++++++++++ .../src/graphql/schema/__init__.py | 4 +- .../src/graphql/schema/metric_schema.py | 12 +++++ .../src/graphql/service/__init__.py | 3 +- .../src/graphql/service/metric_service.py | 42 ++++++++++++++++ 6 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py create mode 100644 forms-flow-data-layer/src/graphql/schema/metric_schema.py create mode 100644 forms-flow-data-layer/src/graphql/service/metric_service.py diff --git a/forms-flow-data-layer/src/graphql/resolvers/__init__.py b/forms-flow-data-layer/src/graphql/resolvers/__init__.py index fed9ed2f03..4de609ff8f 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/__init__.py +++ b/forms-flow-data-layer/src/graphql/resolvers/__init__.py @@ -1,9 +1,10 @@ import strawberry from src.graphql.resolvers.form_resolvers import QueryFormsResolver +from src.graphql.resolvers.metric_resolvers import QueryMetricResolver from src.graphql.resolvers.submission_resolvers import QuerySubmissionsResolver @strawberry.type -class Query(QuerySubmissionsResolver, QueryFormsResolver): # Inherit from query classes +class Query(QuerySubmissionsResolver, QueryMetricResolver, QueryFormsResolver): # Inherit from query classes pass diff --git a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py new file mode 100644 index 0000000000..147b2b2632 --- /dev/null +++ b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py @@ -0,0 +1,48 @@ +from typing import Optional, List + +import strawberry + +from src.graphql.schema import MetricSchema +from src.graphql.service import MetricService +from src.middlewares.auth import auth + + +@strawberry.type +class QueryMetricResolver: + @strawberry.field(extensions=[auth.auth_required()]) + async def get_submission_status_metrics( + self, + info: strawberry.Info, + form_id: str, + order_by: str = 'created', + from_date: Optional[str] = None, + to_date: Optional[str] = None, + ) -> List[MetricSchema]: + """ + GraphQL resolver for querying submission status metrics. + + Args: + info (strawberry.Info): GraphQL context information + form_id (str): ID of the form + order_by (str): Filter to sort submissions by (default: 'created') + from_date (Optional[str]): Filter from submission date + to_date (Optional[str]): Filter to submission date + Returns: + List of Metric objects + """ + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters = {} + filters["order_by"] = order_by + if form_id: + filters["latest_form_id"] = form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date + + metrics = await MetricService.get_submission_status_metrics( + user_context=info.context.get("user"), + filters=filters + ) + return metrics diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 3dabd19808..0f7ea8d473 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,9 +1,11 @@ from src.graphql.schema.form_schema import FormSchema +from src.graphql.schema.metric_schema import MetricSchema from src.graphql.schema.submission_schema import SubmissionSchema from src.middlewares.pagination import PaginationWindow __all__ = [ "FormSchema", + "MetricSchema", + "SubmissionSchema", "PaginationWindow", - "SubmissionSchema" ] diff --git a/forms-flow-data-layer/src/graphql/schema/metric_schema.py b/forms-flow-data-layer/src/graphql/schema/metric_schema.py new file mode 100644 index 0000000000..d6f9224ea3 --- /dev/null +++ b/forms-flow-data-layer/src/graphql/schema/metric_schema.py @@ -0,0 +1,12 @@ +import strawberry + + +@strawberry.type +class MetricSchema: + """ + GraphQL type representing a Metric + This is the external representation of your database model + """ + + metric: str + count: int \ No newline at end of file diff --git a/forms-flow-data-layer/src/graphql/service/__init__.py b/forms-flow-data-layer/src/graphql/service/__init__.py index 39470e71f4..59f150bad5 100644 --- a/forms-flow-data-layer/src/graphql/service/__init__.py +++ b/forms-flow-data-layer/src/graphql/service/__init__.py @@ -1,4 +1,5 @@ from src.graphql.service.form_service import FormService +from src.graphql.service.metric_service import MetricService from src.graphql.service.submission_service import SubmissionService -__all__ = ["FormService", "SubmissionService"] +__all__ = ["FormService", "MetricService", "SubmissionService"] diff --git a/forms-flow-data-layer/src/graphql/service/metric_service.py b/forms-flow-data-layer/src/graphql/service/metric_service.py new file mode 100644 index 0000000000..1704b4cb8f --- /dev/null +++ b/forms-flow-data-layer/src/graphql/service/metric_service.py @@ -0,0 +1,42 @@ +from typing import List + +from src.graphql.schema import MetricSchema +from src.models.webapi import Application +from src.utils import UserContext, get_logger + +logger = get_logger(__name__) + + +class MetricService(): + """Service class for handling metric related operations.""" + + @classmethod + async def get_submission_status_metrics( + cls, + user_context: UserContext, + filters: dict[str, str] = {}, + ) -> List[MetricSchema]: + """ + Fetches aggregated submission status metrics. + + Args: + user_context (UserContext): User context information + filters (dict): Search filters to apply to the query + Returns: + List of Metric objects + """ + result = {} + + # Query webapi database + submissions = await Application.find_all(**filters) + + # Build metrics + for s in submissions: + status = s.application_status + result[status] = result.get(status, 0) + 1 + + # Convert to GraphQL Schema + metrics = [] + for key, value in result: + metrics.append(MetricSchema(metric=key, count=value)) + return metrics From 9c957019de94c77ce2e12e8a9f00ba60ec93a843 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Thu, 31 Jul 2025 10:57:29 -0400 Subject: [PATCH 12/16] Update metricsService --- .../src/graphql/resolvers/form_resolvers.py | 7 +- .../src/graphql/resolvers/metric_resolvers.py | 45 ++++++++- .../src/graphql/service/metric_service.py | 20 ++-- .../src/models/formio/submission.py | 2 +- .../src/models/webapi/application.py | 31 +++++++ .../apiManager/services/metricsServices.js | 80 ++++++++++------ .../components/Dashboard/CardFormCounter.js | 8 +- .../src/components/Dashboard/Dashboard.js | 3 +- .../src/components/Dashboard/StatusChart.js | 93 ++++++++++--------- 9 files changed, 195 insertions(+), 94 deletions(-) diff --git a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py index c3584e225c..9dd10206e8 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/form_resolvers.py @@ -14,7 +14,7 @@ async def get_forms( self, info: strawberry.Info, limit: int = 100, - offset: int = 0, + page_no: int = 1, order_by: str = 'created', type: Optional[str] = None, created_by: Optional[str] = None, @@ -30,7 +30,7 @@ async def get_forms( Args: info (strawberry.Info): GraphQL context information limit (int): Number of items to return (default: 100) - offset (int): Pagination offset (default: 0) + page_no (int): Pagination number (default: 1) order_by (str): Filter to sort forms by (default: 'created') type (Optional[str]): Filter on form type created_by (Optional[str]): Filter on user who created the form @@ -60,6 +60,9 @@ async def get_forms( filters["from_date"] = from_date if to_date: filters["to_date"] = to_date + + # Convert page_no to offset + offset = (page_no - 1) * limit forms = await FormService.get_forms( user_context=info.context.get("user"), diff --git a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py index 147b2b2632..197d93d302 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py @@ -10,7 +10,7 @@ @strawberry.type class QueryMetricResolver: @strawberry.field(extensions=[auth.auth_required()]) - async def get_submission_status_metrics( + async def get_metrics_submission_status( self, info: strawberry.Info, form_id: str, @@ -41,8 +41,49 @@ async def get_submission_status_metrics( if to_date: filters["to_date"] = to_date - metrics = await MetricService.get_submission_status_metrics( + metrics = await MetricService.get_submission_metrics( user_context=info.context.get("user"), + metric='application_status', + filters=filters + ) + return metrics + + + @strawberry.field(extensions=[auth.auth_required()]) + async def get_metrics_submission_created_by( + self, + info: strawberry.Info, + form_id: str, + order_by: str = 'created', + from_date: Optional[str] = None, + to_date: Optional[str] = None, + ) -> List[MetricSchema]: + """ + GraphQL resolver for querying submission created by metrics. + + Args: + info (strawberry.Info): GraphQL context information + form_id (str): ID of the form + order_by (str): Filter to sort submissions by (default: 'created') + from_date (Optional[str]): Filter from submission date + to_date (Optional[str]): Filter to submission date + Returns: + List of Metric objects + """ + # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names + # will be applied automatically. Other filters will require additional handling. + filters = {} + filters["order_by"] = order_by + if form_id: + filters["latest_form_id"] = form_id + if from_date: + filters["from_date"] = from_date + if to_date: + filters["to_date"] = to_date + + metrics = await MetricService.get_submission_metrics( + user_context=info.context.get("user"), + metric='created_by', filters=filters ) return metrics diff --git a/forms-flow-data-layer/src/graphql/service/metric_service.py b/forms-flow-data-layer/src/graphql/service/metric_service.py index 1704b4cb8f..f08129381b 100644 --- a/forms-flow-data-layer/src/graphql/service/metric_service.py +++ b/forms-flow-data-layer/src/graphql/service/metric_service.py @@ -11,32 +11,28 @@ class MetricService(): """Service class for handling metric related operations.""" @classmethod - async def get_submission_status_metrics( + async def get_submission_metrics( cls, user_context: UserContext, + metric: str, filters: dict[str, str] = {}, ) -> List[MetricSchema]: """ - Fetches aggregated submission status metrics. + Fetches aggregated submission metrics. Args: user_context (UserContext): User context information + metric (str): The metric to search on. This should be an existing db column. filters (dict): Search filters to apply to the query Returns: List of Metric objects """ - result = {} - # Query webapi database - submissions = await Application.find_all(**filters) - - # Build metrics - for s in submissions: - status = s.application_status - result[status] = result.get(status, 0) + 1 + webapi_query = await Application.find_aggregated_application_metrics(metric, **filters) + webapi_result = (await Application.execute(webapi_query)).all() # Convert to GraphQL Schema metrics = [] - for key, value in result: - metrics.append(MetricSchema(metric=key, count=value)) + for wr in webapi_result: + metrics.append(MetricSchema(metric=wr.metric, count=wr.count)) return metrics diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index 6db69f4649..efd8deb13a 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -14,7 +14,7 @@ class Settings: name = FormioTables.SUBMISSIONS.value @classmethod - async def count(cls, **filters): + async def count(cls, filters): """Count number of entries that match the passed filters.""" query = cls.find_all() for filter, value in filters.items(): diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index fd0946d26d..9c22c25ef6 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -1,6 +1,7 @@ from datetime import datetime from sqlalchemy import and_, or_, select +from sqlalchemy.sql import func from .authorization import Authorization, AuthType from .base import BaseModel @@ -65,6 +66,36 @@ def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): limit = 5 return query.limit(limit).offset((page_no - 1) * limit) + @classmethod + async def find_aggregated_application_metrics(cls, metric: str, **filters): + """Fetch application metrics.""" + table = await cls.get_table() + + if not hasattr(table.c, metric): + return None + + metric_col = getattr(table.c, metric) + print(metric_col) + query = select( + metric_col.label("metric"), + func.count(table.c.id).label("count") + ).group_by(metric_col) + + # Apply filters + for key, value in filters.items(): + if hasattr(table.c, key): + query = query.where(getattr(table.c, key) == value) + + # Apply date filters, if any + if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): + if from_date := filters.get("from_date"): + query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) + if to_date := filters.get("to_date"): + query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) + + return query + + @classmethod async def get_authorized_applications( cls, diff --git a/forms-flow-web/src/apiManager/services/metricsServices.js b/forms-flow-web/src/apiManager/services/metricsServices.js index 1df069cb57..8f737f9712 100644 --- a/forms-flow-web/src/apiManager/services/metricsServices.js +++ b/forms-flow-web/src/apiManager/services/metricsServices.js @@ -1,4 +1,4 @@ -import { RequestService } from "@formsflow/service"; +import { RequestService, StorageService } from "@formsflow/service"; import API from "../endpoints"; import { setMetricsDateRangeLoading, @@ -27,42 +27,48 @@ export const fetchMetricsSubmissionCount = ( const done = rest.length ? rest[0] : () => { }; return (dispatch) => { dispatch(setMetricsLoadError(false)); - print('formName', formName); + + // Build GraphQL request params const url = API.GRAPHQL; - /*eslint max-len: ["error", { "code": 170 }]*/ + const headers = { + Authorization: `Bearer ${StorageService.get(StorageService.User.AUTH_TOKEN)}`, + 'Content-Type': 'application/json' + }; const query = ` - query FetchMetricsSubmissionQuery { - getSubmission(formName: "${formName}", sortBy: "${sortsBy}", sortOrder: "${sortOrder}", pageNo: ${pageNo}, limit: ${limit}, filters: { - from: "${fromDate}", - to: "${toDate}", - orderBy: "${searchBy}", - }) { - submissions { - applicationStatus - created - createdBy - data - formName + query Query { + getForms( + formName: "${formName}" + fromDate: "${fromDate}" + limit: ${limit} + pageNo: ${pageNo} + orderBy: "${searchBy}" + toDate: "${toDate}" + ) { + items { id - submissionId + parentFormId + totalSubmissions + type + version + status + title } totalCount } } `; - RequestService.httpPOSTRequest(url, { query: query - }) + }, null, true, headers) .then((res) => { if (res.data) { dispatch(setMetricsDateRangeLoading(false)); dispatch(setMetricsLoader(false)); dispatch(setMetricsStatusLoader(false)); - dispatch(setMetricsSubmissionCount(res.data.data.getSubmission.submissions)); - dispatch(setMetricsTotalItems(res.data.data.getSubmission.totalCount)); - if (res.data.applications && res.data.applications[0]) { - dispatch(setSelectedMetricsId(res.data.applications[0].parentFormId)); + dispatch(setMetricsSubmissionCount(res.data.data.getForms.items)); + dispatch(setMetricsTotalItems(res.data.data.getForms.totalCount)); + if (res.data.data.getForms?.items[0]) { + dispatch(setSelectedMetricsId(res.data.data.getForms.items[0].parentFormId)); } else { dispatch(setSelectedMetricsId(null)); @@ -92,7 +98,7 @@ export const fetchMetricsSubmissionStatusCount = ( id, fromDate, toDate, - setSearchBy, + searchBy, options = {}, ...rest ) => { @@ -102,12 +108,32 @@ export const fetchMetricsSubmissionStatusCount = ( dispatch(setSelectedMetricsId(id)); } - RequestService.httpGETRequest( - `${API.METRICS_SUBMISSIONS}/${id}?from=${fromDate}&to=${toDate}&orderBy=${setSearchBy}&formType=${options.parentId ? "parent" : "form"}` - ) + // Build GraphQL request params + const url = API.GRAPHQL; + const headers = { + Authorization: `Bearer ${StorageService.get(StorageService.User.AUTH_TOKEN)}`, + 'Content-Type': 'application/json' + }; + const query = ` + query Query { + getMetricsSubmissionStatus( + formId: "${id}" + fromDate: "${fromDate}" + orderBy: "${toDate}" + toDate: "${searchBy}" + ) { + count + metric + } + } + `; + + RequestService.httpPOSTRequest(url, { + query: query + }, null, true, headers) .then((res) => { if (res.data) { - dispatch(setMetricsSubmissionStatusCount(res.data.applications)); + dispatch(setMetricsSubmissionStatusCount(res.data.data.getMetricsSubmissionStatus)); dispatch(setMetricsStatusLoader(false)); // dispatch(setMetricsTotalItems(res.data.totalCount)); done(null, res.data); diff --git a/forms-flow-web/src/components/Dashboard/CardFormCounter.js b/forms-flow-web/src/components/Dashboard/CardFormCounter.js index 958ee4e5cc..ddc8f72887 100644 --- a/forms-flow-web/src/components/Dashboard/CardFormCounter.js +++ b/forms-flow-web/src/components/Dashboard/CardFormCounter.js @@ -8,7 +8,7 @@ const CardFormCounter = React.memo((props) => { const selectedMetricsId = useSelector( (state) => state.metrics?.selectedMetricsId ); - const { formName, parentFormId, applicationCount } = submissionData; + const { title, parentFormId, totalSubmissions } = submissionData; return (
{ delay={{ show: 0, hide: 400 }} overlay={(propsData) => ( - {formName} + {title} )} > - {formName} + {title}
@@ -43,7 +43,7 @@ const CardFormCounter = React.memo((props) => {
-
{applicationCount}
+
{totalSubmissions}
{(t) => t("Total Submissions")}
diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index f47c9da7ac..1eedfc3c66 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -120,8 +120,7 @@ const Dashboard = React.memo(() => { const getFormattedDate = (date) => { return moment .utc(date) - .format("YYYY-MM-DDTHH:mm:ssZ") - .replace(/\+/g, "%2B"); + .format("YYYY-MM-DDTHH:mm:ssZ"); }; useEffect(() => { diff --git a/forms-flow-web/src/components/Dashboard/StatusChart.js b/forms-flow-web/src/components/Dashboard/StatusChart.js index 627f8ab890..f6b7e308b6 100644 --- a/forms-flow-web/src/components/Dashboard/StatusChart.js +++ b/forms-flow-web/src/components/Dashboard/StatusChart.js @@ -31,7 +31,30 @@ ChartJS.register( Tooltip ); -const chartTypes = [ + +const BACKGROUND_COLORS = [ + "#0088FE33", + "#00C49F33", + "#FFBB2833", + "#FF804233", + "#a0519533", + "#d4508733", + "#f95d6a33", + "#ff7c4333", +]; + +const BORDER_COLORS = [ + "#0088FE", + "#00C49F", + "#FFBB28", + "#FF8042", + "#a05195", + "#d45087", + "#f95d6a", + "#ff7c43", +]; + +const CHART_TYPES = [ { value: 'pie', label: 'Pie Chart' }, { value: 'v-bar', label: 'Vertical Bar Chart' }, { value: 'doughnut', label: 'Doughnut Chart' }, @@ -39,35 +62,31 @@ const chartTypes = [ { value: 'radar', label: 'Radar Chart' }, ]; -const testData = { - datasets: [ - { - label: 'Submissions Dataset', - data: [{"Completed": 10}, {"In Progress": 4}, {"Inactive": 3}, {"Other": 1}], - backgroundColor: [ - "#0088FE", - "#00C49F", - "#FFBB28", - "#FF8042", - "#a05195", - "#d45087", - "#f95d6a", - "#ff7c43" - ] - } - ] -}; - const ChartForm = React.memo((props) => { const { submissionsStatusList, submissionData, submissionStatusCountLoader } = props; - const { formName } = submissionData; + const {title} = submissionData; + const { t } = useTranslation(); const [selectedChartValue, setSelectedChartValue] = useState('pie'); - const { t } = useTranslation(); - const chartData = submissionsStatusList || []; - console.log(chartData); + let chartLabels = []; + let chartDataset = []; + submissionsStatusList.map((metric) => { + chartLabels.push(metric.metric); + chartDataset.push(metric.count); + }); + + const chartData = { + labels: chartLabels, + datasets: [{ + label: `${title} Dataset`, + data: chartDataset, + backgroundColor: BACKGROUND_COLORS, + borderColor: BORDER_COLORS, + borderWidth: 1 + }] + } || {}; const chartOptions = { plugins: { @@ -82,35 +101,35 @@ const ChartForm = React.memo((props) => { case 'pie': return ( ); case 'v-bar': return ( ); case 'doughnut': return ( ); case 'polar-area': return ( ); case 'radar': return ( ); @@ -122,20 +141,6 @@ const ChartForm = React.memo((props) => {
-
-
-
- - {t("Form Name")} : - -

- {formName} -

-
-
-
{ title={t("Choose any")} aria-label="Select chart type" > - {chartTypes.map((option, index) => ( + {CHART_TYPES.map((option, index) => ( ))} From 9882419db6deabda989257846b2a2101a8eb7d5d Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Thu, 31 Jul 2025 11:21:52 -0400 Subject: [PATCH 13/16] Undo unneccessary changes to submission graphql queries --- forms-flow-data-layer/src/db/formio_db.py | 4 +- .../src/graphql/resolvers/__init__.py | 4 +- .../src/graphql/resolvers/metric_resolvers.py | 2 +- .../graphql/resolvers/submission_resolvers.py | 101 +++---- .../src/graphql/schema/__init__.py | 8 +- .../src/graphql/schema/metric_schema.py | 2 +- .../src/graphql/schema/submission_schema.py | 67 ++--- .../src/graphql/service/form_service.py | 10 +- .../src/graphql/service/submission_service.py | 259 +++++++++++++----- .../src/middlewares/pagination.py | 2 +- .../src/models/formio/__init__.py | 6 +- .../src/models/formio/form.py | 4 +- .../src/models/formio/submission.py | 74 ++++- .../src/models/webapi/application.py | 55 +++- .../src/models/webapi/form_process_mapper.py | 2 +- 15 files changed, 391 insertions(+), 209 deletions(-) diff --git a/forms-flow-data-layer/src/db/formio_db.py b/forms-flow-data-layer/src/db/formio_db.py index 66d3d8c50e..2cc99ad3e0 100644 --- a/forms-flow-data-layer/src/db/formio_db.py +++ b/forms-flow-data-layer/src/db/formio_db.py @@ -4,7 +4,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from src.config.envs import ENVS -from src.models.formio import Form, Submission # Import your MongoDB models +from src.models.formio import FormModel, SubmissionModel # Import your MongoDB models from src.utils import get_logger logger = get_logger(__name__) @@ -23,7 +23,7 @@ async def init_formio_db(self): self.__client = AsyncIOMotorClient(ENVS.FORMIO_MONGO_DB_URI) self.formio_db = self.__client[ENVS.FORMIO_DB_NAME] await init_beanie( - database=self.formio_db, document_models=[Form, Submission] + database=self.formio_db, document_models=[FormModel, SubmissionModel] ) def get_db(self): diff --git a/forms-flow-data-layer/src/graphql/resolvers/__init__.py b/forms-flow-data-layer/src/graphql/resolvers/__init__.py index 4de609ff8f..f039ef5c40 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/__init__.py +++ b/forms-flow-data-layer/src/graphql/resolvers/__init__.py @@ -1,10 +1,10 @@ import strawberry from src.graphql.resolvers.form_resolvers import QueryFormsResolver -from src.graphql.resolvers.metric_resolvers import QueryMetricResolver +from src.graphql.resolvers.metric_resolvers import QueryMetricsResolver from src.graphql.resolvers.submission_resolvers import QuerySubmissionsResolver @strawberry.type -class Query(QuerySubmissionsResolver, QueryMetricResolver, QueryFormsResolver): # Inherit from query classes +class Query(QuerySubmissionsResolver, QueryMetricsResolver, QueryFormsResolver): # Inherit from query classes pass diff --git a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py index 197d93d302..844e61422c 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py @@ -8,7 +8,7 @@ @strawberry.type -class QueryMetricResolver: +class QueryMetricsResolver: @strawberry.field(extensions=[auth.auth_required()]) async def get_metrics_submission_status( self, diff --git a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py index bc962973f8..de82637363 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/submission_resolvers.py @@ -1,91 +1,56 @@ -from typing import Optional +from typing import List, Optional import strawberry +from strawberry.scalars import JSON -from src.graphql.schema import ( - PaginationWindow, - SubmissionSchema, -) +from src.graphql.schema import PaginatedSubmissionResponse from src.graphql.service import SubmissionService from src.middlewares.auth import auth @strawberry.type class QuerySubmissionsResolver: - @strawberry.field(extensions=[auth.auth_required()]) - async def get_submissions( - self, - info: strawberry.Info, - limit: int = 100, - offset: int = 0, - order_by: str = 'created', - application_status: Optional[str] = None, - form_name: Optional[str] = None, - form_type: Optional[str] = None, - parent_form_id: Optional[str] = None, - from_date: Optional[str] = None, - to_date: Optional[str] = None - ) -> PaginationWindow[SubmissionSchema]: - """ - GraphQL resolver for querying submissions. - - Args: - info (strawberry.Info): GraphQL context information - limit (int): Number of items to return (default: 100) - offset (int): Pagination offset (default: 0) - order_by (str): Filter to sort submissions by (default: 'created') - application_status (Optional[str]): Filter on submission status - form_name (Optional[str]): Filter on form name - form_type (Optional[str]): Filter on form type - parent_form_id (Optional[str]): Filter on form parent id - from_date (Optional[str]): Filter from submission date - to_date (Optional[str]): Filter to submission date - Returns: - Paginated list of Submission objects containing combined PostgreSQL and MongoDB data - """ - # Create filters dict. Filters that share names with PostgreSQL or MongoDB column names - # will be applied automatically. Other filters will require additional handling. - filters = {} - filters["order_by"] = order_by - if application_status: - filters["application_status"] = application_status - if form_name: - filters["form_name"] = form_name - if form_type: - filters["form_type"] = form_type - if parent_form_id: - filters["parent_form_id"] = parent_form_id - if from_date: - filters["from_date"] = from_date - if to_date: - filters["to_date"] = to_date - - forms = await SubmissionService.get_submissions( - user_context=info.context.get("user"), - limit=limit, - offset=offset, - filters=filters - ) - return forms - - @strawberry.field(extensions=[auth.auth_required()]) async def get_submission( self, info: strawberry.Info, - submission_id: str, - ) -> Optional[SubmissionSchema]: + sort_by: str = "created", + sort_order: str = "desc", + parent_form_id: Optional[str] = None, + filters: Optional[JSON] = None, + selected_form_fields: Optional[List[str]] = None, + created_before: Optional[str] = None, + created_after: Optional[str] = None, + page_no: int = 1, + limit: int = 5, + ) -> Optional[PaginatedSubmissionResponse]: """ GraphQL resolver for querying submission. Args: info (strawberry.Info): GraphQL context information - submission_id (str): ID of the submission + sort_by (str): Field to sort by (default: "created") + sort_order (str): Order of sorting (default: "desc") + parent_form_id (Optional[str]): ID of the parent form + filters (Optional[JSON]): Filters to apply to the query + selected_form_fields (Optional[List[str]]): Form fields to include in the response + created_before (Optional[str]): Filter for submissions created before this date + created_after (Optional[str]): Filter for submissions created after this date + page_no (int): Page number for pagination (default: 1) + limit (int): Number of items per page (default: 5) Returns: - Submission object containing combined PostgreSQL and MongoDB data + Submission object containing combined SQL and MongoDB data """ submission = await SubmissionService.get_submission( - user_context=info.context.get("user"), - submission_id=submission_id, + info=info, + sort_by=sort_by, + sort_order=sort_order, + parent_form_id=parent_form_id, + filters=filters, + selected_form_fields=selected_form_fields, + created_before=created_before, + created_after=created_after, + page_no=page_no, + limit=limit, ) return submission diff --git a/forms-flow-data-layer/src/graphql/schema/__init__.py b/forms-flow-data-layer/src/graphql/schema/__init__.py index 0f7ea8d473..fd1b5ea82d 100644 --- a/forms-flow-data-layer/src/graphql/schema/__init__.py +++ b/forms-flow-data-layer/src/graphql/schema/__init__.py @@ -1,11 +1,17 @@ from src.graphql.schema.form_schema import FormSchema from src.graphql.schema.metric_schema import MetricSchema -from src.graphql.schema.submission_schema import SubmissionSchema +from src.graphql.schema.submission_schema import ( + PaginatedSubmissionResponse, + SubmissionDetailsWithSubmissionData, + SubmissionSchema, +) from src.middlewares.pagination import PaginationWindow __all__ = [ "FormSchema", "MetricSchema", "SubmissionSchema", + "SubmissionDetailsWithSubmissionData", + "PaginatedSubmissionResponse", "PaginationWindow", ] diff --git a/forms-flow-data-layer/src/graphql/schema/metric_schema.py b/forms-flow-data-layer/src/graphql/schema/metric_schema.py index d6f9224ea3..8477df4a9c 100644 --- a/forms-flow-data-layer/src/graphql/schema/metric_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/metric_schema.py @@ -9,4 +9,4 @@ class MetricSchema: """ metric: str - count: int \ No newline at end of file + count: int diff --git a/forms-flow-data-layer/src/graphql/schema/submission_schema.py b/forms-flow-data-layer/src/graphql/schema/submission_schema.py index 82410df9ac..1ce4b436b5 100644 --- a/forms-flow-data-layer/src/graphql/schema/submission_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/submission_schema.py @@ -1,61 +1,40 @@ """Managing webapi schemas.""" -from typing import Optional +from typing import List, Optional import strawberry +from strawberry.scalars import JSON @strawberry.type class SubmissionSchema: """ - GraphQL type representing a Submission + GraphQL type representing a Application This is the external representation of your database model """ - # WebAPI populated fields id: int application_status: str - form_id: str - submission_id: str - created_at: str - updated_at: str + task_name: str + data: Optional[strawberry.scalars.JSON] # Field to hold arbitrary JSON data + + +@strawberry.type +class SubmissionDetailsWithSubmissionData: + id: int created_by: str - updated_by: str - is_resubmit: bool - is_draft: bool + submission_id: str + form_name: str + application_status: str + created: str + data: Optional[JSON] = ( + None # this data is the submission data from mongodb or we can pass any json data + ) - # FormIO populated fields - data: Optional[strawberry.scalars.JSON] # Field to hold arbitrary JSON data - # BPM populated fields - # None - - # Calculated fields - # None - - @staticmethod - def from_result(result: dict): - data = {} - - # Map WebAPI data - if webapi := result.get("webapi"): - data.update({ - "id": webapi.id, - "application_status": webapi.application_status, - "form_id": webapi.latest_form_id, - "submission_id": webapi.submission_id, - "created_at": (webapi.created.isoformat() if webapi.created else None), - "updated_at": (webapi.modified.isoformat() if webapi.modified else None), - "created_by": webapi.created_by, - "updated_by": webapi.modified_by, - "is_resubmit": webapi.is_resubmit, - "is_draft": webapi.is_draft - }) - - # Map FormIO data - if formio := result.get("formio"): - data.update({ - "data": formio.data - }) - - return SubmissionSchema(**data) if data else None +@strawberry.type +class PaginatedSubmissionResponse: + submissions: List[SubmissionDetailsWithSubmissionData] + total_count: int + page_no: Optional[int] = None + limit: Optional[int] = None diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index 563e813c05..d1bf9a8fc9 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -4,7 +4,7 @@ from src.graphql.schema import FormSchema, PaginationWindow from src.middlewares.pagination import verify_pagination_params -from src.models.formio import Form, Submission +from src.models.formio import FormModel, SubmissionModel from src.models.webapi import FormProcessMapper from src.utils import UserContext, get_logger @@ -45,10 +45,10 @@ async def get_forms( results = [] webapi_results = (await FormProcessMapper.execute(webapi_query)).all() for wr in webapi_results: - submissions_count = await Submission.count(filters={"form": PydanticObjectId(wr.form_id)}) + submissions_count = await SubmissionModel.count(filters={"form": PydanticObjectId(wr.form_id)}) results.append({ "webapi": wr, - "formio": await Form.get(PydanticObjectId(wr.form_id)), + "formio": await FormModel.get(PydanticObjectId(wr.form_id)), "calculated": {"total_submissions": submissions_count} }) @@ -74,8 +74,8 @@ async def get_form( """ # Query the databases webapi_result = await FormProcessMapper.first(form_id=form_id) - formio_result = await Form.get(PydanticObjectId(form_id)) - submissions_count = await Submission.count(filters={"form": PydanticObjectId(webapi_result.form_id)}) + formio_result = await FormModel.get(PydanticObjectId(form_id)) + submissions_count = await SubmissionModel.count(filters={"form": PydanticObjectId(webapi_result.form_id)}) # Combine results result = { diff --git a/forms-flow-data-layer/src/graphql/service/submission_service.py b/forms-flow-data-layer/src/graphql/service/submission_service.py index 521a916d30..16a0ff56b7 100644 --- a/forms-flow-data-layer/src/graphql/service/submission_service.py +++ b/forms-flow-data-layer/src/graphql/service/submission_service.py @@ -1,86 +1,197 @@ -from typing import Optional +from typing import Any, Dict, List, Optional, Tuple -from beanie import PydanticObjectId +import strawberry -from src.graphql.schema import PaginationWindow, SubmissionSchema -from src.middlewares.pagination import verify_pagination_params -from src.models.formio import Submission -from src.models.webapi import Application -from src.utils import UserContext, get_logger +from src.graphql.schema import ( + PaginatedSubmissionResponse, + SubmissionDetailsWithSubmissionData, +) +from src.models.formio.submission import SubmissionModel +from src.models.webapi.application import Application +from src.utils import get_logger logger = get_logger(__name__) -class SubmissionService(): +class SubmissionService: """Service class for handling submission related operations on mongo and webapi side.""" - @classmethod - @verify_pagination_params - async def get_submissions( - cls, - user_context: UserContext, - limit: int = 100, - offset: int = 0, - filters: dict[str, str] = {}, - ) -> PaginationWindow[SubmissionSchema]: - """ - Fetches submissions from the WebAPI and adds additional details from FormIO. - - Args: - user_context (UserContext): User context information - limit (int): Number of items to return (default: 100) - offset (int): Pagination offset (default: 0) - filters (dict): Search filters to apply to the query - Returns: - Paginated list of Submission objects containing combined PostgreSQL and MongoDB data - """ - # Query webapi database - webapi_query = await Application.find_all(**filters) - webapi_total_count =await Application.count(**filters) - - # Apply pagination filters - webapi_query = webapi_query.offset(offset).limit(limit) - - # Combine results with data from formio - results = [] - webapi_results = (await Application.execute(webapi_query)).all() - for wr in webapi_results: - results.append({ - "webapi": wr, - "formio": await Submission.get(PydanticObjectId(wr.submission_id)), - }) - - # Convert to GraphQL Schema - submissions = [SubmissionSchema.from_result(result=r) for r in results] - return PaginationWindow(items=submissions, total_count=webapi_total_count) - - - @classmethod + @staticmethod + async def split_search_criteria( + search: Optional[Dict[str, Any]], webapi_fields: List[str] + ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """Splits search criteria into webapi and mongo search dictionaries.""" + webapi_search = {} + mongo_search = {} + + if search: + for field, value in search.items(): + if field in webapi_fields: + webapi_search[field] = value + else: + mongo_search[field] = value + + return webapi_search, mongo_search + + @staticmethod + def _process_results( + webapi_side_submissions, mongo_side_submissions, is_sort_on_webapi_side, limit + ): + """Process results and merge with MongoDB data.""" + + # Pre-process MongoDB submissions once (convert _id to submission_id and create lookup dict) + mongo_dict = {} + lookup_key = "submission_id" + for sub in mongo_side_submissions: + submission_id = sub.pop( + "_id" + ) # Remove the _id field from the submission data + mongo_dict[submission_id] = sub + + # Create a single processing path regardless of sort origin + if is_sort_on_webapi_side: + source_data = webapi_side_submissions + lookup_dict = mongo_dict + else: + source_data = [ + {"submission_id": sid} for sid in mongo_dict.keys() + ] # instead of full data just mock the data by giving the value as submission_id + lookup_dict = { + app["submission_id"]: app + for app in webapi_side_submissions + if app["submission_id"] + } + + # Single merging logic + final_results = [] + for item in source_data: + submission_id = item[lookup_key] + if submission_id in lookup_dict: + # the variable item and lookup_dict[submission_id] are same in the case of webapi side sort + base_data = ( + item if is_sort_on_webapi_side else lookup_dict[submission_id] + ) + final_results.append( + {**base_data, "submission_data": mongo_dict[submission_id]} + ) + # Apply limit if provided + if limit and len(final_results) >= limit: + break + + return final_results + + @staticmethod async def get_submission( - cls, - user_context: UserContext, - submission_id: str - ) -> Optional[SubmissionSchema]: + info: strawberry.Info, + sort_by: str, + sort_order: str, + parent_form_id: str, + filters: Dict, + selected_form_fields: List[str], + created_before: Optional[str], + created_after: Optional[str], + page_no: int, + limit: int, + ) -> Optional[PaginatedSubmissionResponse]: """ - Fetches a submission based on it's submission_id from the WebAPI and adds additional details from FormIO. - - Args: - user_context (UserContext): User context information - submission_id (str): ID of the submission + Fetches submissions from both webapi and MongoDB, merges them, and returns a paginated response. + Args:sort_by: Field to sort by (default: "created") + sort_order: Order of sorting (default: "desc") + parent_form_id: ID of the parent form + filters: Filters to apply to the query + selected_form_fields: Fields to select from MongoDB + page_no: Page number for pagination + limit: Number of records per page Returns: - Submission object containing combined PostgreSQL and MongoDB data + PaginatedSubmissionResponse: A paginated response containing submissions + and total count. """ - # Query the databases - webapi_result = await Application.first(submission_id=submission_id) - formio_result = await Submission.get(PydanticObjectId(submission_id)) - - # Combine results - result = { - "webapi": webapi_result, - "formio": formio_result, - } - - # Convert to GraphQL Schema - submission = SubmissionSchema.from_result(result=result) - return submission - + # Get user context from token + user = info.context["user"] + tenant_key = user.tenant_key + user_groups = user.token_info.get("groups", []) + webapi_fields = [ + "created_by", + "application_status", + "id", + "created", + "form_name", + ] + # drived filter mongo serach and webapi search + webapi_search, mongo_search = await SubmissionService.split_search_criteria( + filters, webapi_fields + ) + logger.info( + f"extracted filter by mongo {mongo_search} and webapi {webapi_search}" + ) + + is_paginate_on_webapi_side = not mongo_search + is_sort_on_webapi_side = sort_by in webapi_fields + sort_params = {"sort_by": sort_by, "sort_order": sort_order} + webapi_side_submissions, total_count = ( + await Application.get_authorized_applications( + tenant_key=tenant_key, + roles=user_groups, + is_paginate=is_paginate_on_webapi_side, + filter=webapi_search, + created_before=created_before, + created_after=created_after, + page_no=page_no, + limit=limit, + parent_form_id=parent_form_id, + **(sort_params if is_sort_on_webapi_side else {}), + ) + ) + + mongo_side_submissions = {} + final_out_puts = None + needs_mongo_submissions = ( + webapi_side_submissions and parent_form_id + ) # if parent_form_id and webapi side submission is non empty then only go to mongo side + if needs_mongo_submissions: + logger.info("Fetching submission data from formio.") + # Extract the submission IDs + submission_ids = [ + app["submission_id"] + for app in webapi_side_submissions + if app["submission_id"] + ] + # Get filtered submissions from MongoDB + mongo_side_submissions = await SubmissionModel.query_submission( + submission_ids=submission_ids, + filter=mongo_search, + selected_form_fields=selected_form_fields, + page_no=not is_paginate_on_webapi_side and page_no or None, + limit=not is_paginate_on_webapi_side and limit or None, + **(sort_params if not is_sort_on_webapi_side else {}), + ) + final_out_puts = SubmissionService._process_results( + webapi_side_submissions, + mongo_side_submissions.get("submissions", []), + is_sort_on_webapi_side, + limit, + ) + # sometimes webapi_side_submission will be no empty but mongo side submission will be empty + data = final_out_puts if needs_mongo_submissions else webapi_side_submissions + return PaginatedSubmissionResponse( + submissions=[ + SubmissionDetailsWithSubmissionData( + id=row.get("id"), + form_name=row.get("form_name"), + submission_id=row.get("submission_id"), + created_by=row.get("created_by"), + application_status=row.get("application_status"), + created=row.get("created"), + data=row.get("submission_data", {}), + ) + for row in data + ], + total_count=( + mongo_side_submissions.get("total_count", 0) + if mongo_search + and needs_mongo_submissions + else total_count + ), + page_no=page_no, + limit=limit, + ) diff --git a/forms-flow-data-layer/src/middlewares/pagination.py b/forms-flow-data-layer/src/middlewares/pagination.py index dc874bbe97..60eb780a76 100644 --- a/forms-flow-data-layer/src/middlewares/pagination.py +++ b/forms-flow-data-layer/src/middlewares/pagination.py @@ -21,4 +21,4 @@ def wrapper(*args, **kwargs): if (offset := kwargs.get('offset')) and offset < 0: raise Exception(f"offset ({offset}) must be greater than 0") return function(*args, **kwargs) - return wrapper \ No newline at end of file + return wrapper diff --git a/forms-flow-data-layer/src/models/formio/__init__.py b/forms-flow-data-layer/src/models/formio/__init__.py index 78363704f8..61945509d8 100644 --- a/forms-flow-data-layer/src/models/formio/__init__.py +++ b/forms-flow-data-layer/src/models/formio/__init__.py @@ -1,5 +1,5 @@ from src.models.formio.constants import FormioTables -from src.models.formio.form import Form -from src.models.formio.submission import Submission +from src.models.formio.form import FormModel +from src.models.formio.submission import SubmissionModel -__all__ = ["Form", "Submission", "FormioTables"] +__all__ = ["FormModel", "SubmissionModel", "FormioTables"] diff --git a/forms-flow-data-layer/src/models/formio/form.py b/forms-flow-data-layer/src/models/formio/form.py index 7e5a2f308a..468364addd 100644 --- a/forms-flow-data-layer/src/models/formio/form.py +++ b/forms-flow-data-layer/src/models/formio/form.py @@ -6,12 +6,12 @@ from .constants import FormioTables -class Form(Document): +class FormModel(Document): title: str name: str path: str type: str - isBundle: bool + isBundle: Optional[bool] display: Optional[str] = None created: Optional[datetime] = None modified: Optional[datetime] = None diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index efd8deb13a..dd171e992a 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -6,8 +6,9 @@ from .constants import FormioTables -class Submission(Document): +class SubmissionModel(Document): data: dict + _id: PydanticObjectId form: PydanticObjectId class Settings: @@ -21,3 +22,74 @@ async def count(cls, filters): if hasattr(cls, filter): query = query.find(getattr(cls, filter) == value) return (await query.count()) + + @staticmethod + def _build_match_stage(submission_ids: List[str], filter: Optional[dict]) -> dict: + """Build the MongoDB match stage.""" + match_stage = {} + if submission_ids: + match_stage["_id"] = {"$in": [ObjectId(id) for id in submission_ids]} + if filter: + for field, value in filter.items(): + match_stage[f"data.{field}"] = {"$regex": value, "$options": "i"} + return match_stage + + @staticmethod + def _build_sort_stage(sort_by: str, sort_order: str) -> Optional[dict]: + """Build the MongoDB sort stage if needed.""" + if not sort_by: + return None + sort_field = f"data.{sort_by}" + sort_value = 1 if sort_order.lower() == "asc" else -1 + return {"$sort": {sort_field: sort_value}} + + @staticmethod + def _build_projection_stage(project_fields: Optional[List[str]]) -> dict: + """Build the MongoDB projection stage.""" + project_stage = {"$project": { + "_id": {"$toString": "$_id"}, + "created": 1 + }} + if project_fields: + for field in project_fields: + project_stage["$project"][field] = f"$data.{field}" + return project_stage + + @staticmethod + async def query_submission( + submission_ids: List[str], + filter: Optional[dict] = None, + selected_form_fields: Optional[List[str]] = None, + page_no: Optional[int] = None, + limit: Optional[int] = None, + sort_by: Optional[str] = None, + sort_order: str = "asc", + ) -> Dict[str, Any]: + """ + Query submissions from MongoDB with optional pagination and sorting. + """ + # Build match stage + match_stage = Submission._build_match_stage( + submission_ids=submission_ids, filter=filter + ) + pipeline = [{"$match": match_stage}] + + # Add sorting if sort_by is specified + if sort_stage := Submission._build_sort_stage(sort_by, sort_order): + pipeline.append(sort_stage) + + # Projection stage + pipeline.append(Submission._build_projection_stage(selected_form_fields)) + # Only add pagination if page_no and limit specified + if page_no is not None and limit is not None: + total = await Submission.aggregate(pipeline).count() + pipeline.append({"$skip": (page_no - 1) * limit}) + pipeline.append({"$limit": limit}) + items = await Submission.aggregate(pipeline).to_list() + else: + items = await Submission.aggregate(pipeline).to_list() + total = len(items) + return { + "submissions": items, + "total_count": total, + } \ No newline at end of file diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 9c22c25ef6..b430596ac6 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -67,6 +67,7 @@ def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): return query.limit(limit).offset((page_no - 1) * limit) @classmethod + async def find_aggregated_application_metrics(cls, metric: str, **filters): """Fetch application metrics.""" table = await cls.get_table() @@ -75,7 +76,6 @@ async def find_aggregated_application_metrics(cls, metric: str, **filters): return None metric_col = getattr(table.c, metric) - print(metric_col) query = select( metric_col.label("metric"), func.count(table.c.id).label("count") @@ -96,11 +96,47 @@ async def find_aggregated_application_metrics(cls, metric: str, **filters): return query + @classmethod + def filter_query(cls, query, filter: dict, application_table): + """ + Apply filters to the SQLAlchemy query. + """ + for field, value in filter.items(): + if hasattr(application_table.c, field): + col = getattr(application_table.c, field) + if field == "id": + # Special case for application_id + query = query.where(col == value) + else: + # For other fields, use ilike for case-insensitive search + query = query.where(col.ilike(f"%{value}%")) + return query + + @classmethod + def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): + """ + Paginate the SQLAlchemy query. + """ + if page_no < 1: + page_no = 1 + if limit < 1: + limit = 5 + return query.limit(limit).offset((page_no - 1) * limit) + @classmethod async def get_authorized_applications( cls, tenant_key: str, roles: list[str], + parent_form_id: str, + filter: dict = None, + created_before: str = None, + created_after: str = None, + sort_by: str = None, + sort_order: str = None, + is_paginate: bool = False, + page_no: int = 1, + limit: int = 5, ): """ Fetches authorized applications based on the provided parameters. @@ -108,6 +144,14 @@ async def get_authorized_applications( application_table = await cls.get_table() mapper_table = await FormProcessMapper.get_table() authorization_table = await Authorization.get_table() + # ["created_by", "application_status", "id", "created", "form_name"] + sortable_fields = { + "application_status": application_table.c.application_status, + "id": application_table.c.id, + "created_by": application_table.c.created_by, + "created": application_table.c.created, + "form_name": mapper_table.c.form_name, + } query = select( application_table, mapper_table.c.parent_form_id, mapper_table.c.form_name @@ -118,12 +162,17 @@ async def get_authorized_applications( authorization_table=authorization_table, roles=roles ) + # Optional condition + parent_form_id_condition = ( + mapper_table.c.parent_form_id == parent_form_id if parent_form_id else True + ) # Join tables query = query.join( mapper_table, and_( application_table.c.form_process_mapper_id == mapper_table.c.id, + parent_form_id_condition, # Ensure parent form ID matches if provided mapper_table.c.tenant == tenant_key, # Ensure tenant key matches in both tables ), @@ -172,6 +221,6 @@ async def get_authorized_applications( result = await cls.execute(query) applications = result.mappings().all() - total_count = len(applications) + total_count = len(applications) if not is_paginate else total_count - return applications, total_count + return applications, total_count \ No newline at end of file diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py index 700a0b7887..78fbd13d07 100644 --- a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -29,4 +29,4 @@ async def find_all(cls, **filters): if to_date := filters.get("to_date"): query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) - return query \ No newline at end of file + return query From 6ef433bfe16958e40bf86ed3fdc9a893f32e4312 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Thu, 31 Jul 2025 12:17:49 -0400 Subject: [PATCH 14/16] Fix rebase issues --- .../src/middlewares/role_check.py | 2 -- .../src/models/formio/submission.py | 26 ++++++++++++------- .../src/models/webapi/application.py | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/forms-flow-data-layer/src/middlewares/role_check.py b/forms-flow-data-layer/src/middlewares/role_check.py index 7d6232b589..3930a7d0d8 100644 --- a/forms-flow-data-layer/src/middlewares/role_check.py +++ b/forms-flow-data-layer/src/middlewares/role_check.py @@ -3,8 +3,6 @@ import strawberry from strawberry.extensions import FieldExtension -# Currently not used in favour of extending BasePermission in auth.py -# See here for more details: https://strawberry.rocks/docs/guides/permissions class RoleCheck(FieldExtension): """Custom extension.""" diff --git a/forms-flow-data-layer/src/models/formio/submission.py b/forms-flow-data-layer/src/models/formio/submission.py index dd171e992a..626ec68b11 100644 --- a/forms-flow-data-layer/src/models/formio/submission.py +++ b/forms-flow-data-layer/src/models/formio/submission.py @@ -31,7 +31,12 @@ def _build_match_stage(submission_ids: List[str], filter: Optional[dict]) -> dic match_stage["_id"] = {"$in": [ObjectId(id) for id in submission_ids]} if filter: for field, value in filter.items(): - match_stage[f"data.{field}"] = {"$regex": value, "$options": "i"} + if isinstance(value, str): + # Only use regex for string values + match_stage[f"data.{field}"] = {"$regex": value, "$options": "i"} + else: + # For non-string values (numbers, booleans, etc.), use exact match + match_stage[f"data.{field}"] = value return match_stage @staticmethod @@ -48,7 +53,6 @@ def _build_projection_stage(project_fields: Optional[List[str]]) -> dict: """Build the MongoDB projection stage.""" project_stage = {"$project": { "_id": {"$toString": "$_id"}, - "created": 1 }} if project_fields: for field in project_fields: @@ -69,27 +73,31 @@ async def query_submission( Query submissions from MongoDB with optional pagination and sorting. """ # Build match stage - match_stage = Submission._build_match_stage( + match_stage = SubmissionModel._build_match_stage( submission_ids=submission_ids, filter=filter ) pipeline = [{"$match": match_stage}] # Add sorting if sort_by is specified - if sort_stage := Submission._build_sort_stage(sort_by, sort_order): + if sort_stage := SubmissionModel._build_sort_stage(sort_by, sort_order): pipeline.append(sort_stage) # Projection stage - pipeline.append(Submission._build_projection_stage(selected_form_fields)) + pipeline.append(SubmissionModel._build_projection_stage(selected_form_fields)) # Only add pagination if page_no and limit specified if page_no is not None and limit is not None: - total = await Submission.aggregate(pipeline).count() + # Get only the count (no document data) + count_pipeline = pipeline + [{"$count": "total"}] + count_result = await SubmissionModel.aggregate(count_pipeline).to_list(length=1) + total = count_result[0]["total"] if count_result else 0 + # Add skip and limit stages for pagination pipeline.append({"$skip": (page_no - 1) * limit}) pipeline.append({"$limit": limit}) - items = await Submission.aggregate(pipeline).to_list() + items = await SubmissionModel.aggregate(pipeline).to_list() else: - items = await Submission.aggregate(pipeline).to_list() + items = await SubmissionModel.aggregate(pipeline).to_list() total = len(items) return { "submissions": items, "total_count": total, - } \ No newline at end of file + } diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index b430596ac6..0a836e982b 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -223,4 +223,4 @@ async def get_authorized_applications( applications = result.mappings().all() total_count = len(applications) if not is_paginate else total_count - return applications, total_count \ No newline at end of file + return applications, total_count From 76182997a1d6d50828fee2afd8ae912cd20517cf Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Mon, 4 Aug 2025 21:41:26 -0400 Subject: [PATCH 15/16] Apply tenant checks on form graphql query --- .../src/graphql/resolvers/metric_resolvers.py | 2 +- .../src/graphql/schema/form_schema.py | 6 +- .../src/graphql/service/form_service.py | 3 +- forms-flow-data-layer/src/middlewares/auth.py | 18 +----- .../src/models/webapi/application.py | 2 +- .../src/models/webapi/form_process_mapper.py | 63 +++++++++++++++++-- 6 files changed, 67 insertions(+), 27 deletions(-) diff --git a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py index 844e61422c..67b1753fd6 100644 --- a/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py +++ b/forms-flow-data-layer/src/graphql/resolvers/metric_resolvers.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import List, Optional import strawberry diff --git a/forms-flow-data-layer/src/graphql/schema/form_schema.py b/forms-flow-data-layer/src/graphql/schema/form_schema.py index f8a346cf57..982ae1b267 100644 --- a/forms-flow-data-layer/src/graphql/schema/form_schema.py +++ b/forms-flow-data-layer/src/graphql/schema/form_schema.py @@ -2,7 +2,7 @@ import strawberry -from src.middlewares.auth import IsAdmin +from src.middlewares.role_check import RoleCheck @strawberry.type @@ -14,8 +14,10 @@ class FormSchema: # FormIO populated fields id: str + name: Optional[str] = strawberry.field( + extensions=[RoleCheck(["admin"])] + ) title: str - name: Optional[str] = strawberry.field(permission_classes=[IsAdmin]) path: str type: str display: Optional[str] = None diff --git a/forms-flow-data-layer/src/graphql/service/form_service.py b/forms-flow-data-layer/src/graphql/service/form_service.py index d1bf9a8fc9..875646465d 100644 --- a/forms-flow-data-layer/src/graphql/service/form_service.py +++ b/forms-flow-data-layer/src/graphql/service/form_service.py @@ -35,8 +35,7 @@ async def get_forms( Paginated list of Form objects containing combined PostgreSQL and MongoDB data """ # Query webapi database - webapi_query = await FormProcessMapper.find_all(**filters) - webapi_total_count = await FormProcessMapper.count(**filters) + webapi_query, webapi_total_count = await FormProcessMapper.find_all(user_context=user_context, **filters) # Apply pagination filters webapi_query = webapi_query.offset(offset).limit(limit) diff --git a/forms-flow-data-layer/src/middlewares/auth.py b/forms-flow-data-layer/src/middlewares/auth.py index e9251d081c..366935ca59 100644 --- a/forms-flow-data-layer/src/middlewares/auth.py +++ b/forms-flow-data-layer/src/middlewares/auth.py @@ -36,6 +36,8 @@ async def has_permission( token = auth.split(" ")[1] payload = await keycloak_validator.verify_token(token) # Attach token info to context + print('token', token) + print('payload', payload) info.context["user"] = UserContext(token=token, token_info=payload) return True except Exception as e: @@ -43,22 +45,6 @@ async def has_permission( raise GraphQLError(f"Unexpected error: {str(e)}") -class IsAdmin(BasePermission): - """Class for check if user is admin.""" - - message = "User role must be admin" - error_extensions = {"code": "UNAUTHORIZED"} - - async def has_permission( - self, source: Any, info: strawberry.Info, **kwargs - ) -> bool: - try: - user = info.context["user"] - return user.has_any_roles(['admin']) - except Exception as e: - raise GraphQLError(f"Unexpected error: {str(e)}") - - class HasAnyRole(BasePermission): """Class for check authorization.""" diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 0a836e982b..21c35f09d0 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -1,6 +1,6 @@ from datetime import datetime -from sqlalchemy import and_, or_, select +from sqlalchemy import and_, desc, or_, select from sqlalchemy.sql import func from .authorization import Authorization, AuthType diff --git a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py index 78fbd13d07..7d56c30c69 100644 --- a/forms-flow-data-layer/src/models/webapi/form_process_mapper.py +++ b/forms-flow-data-layer/src/models/webapi/form_process_mapper.py @@ -1,5 +1,14 @@ from datetime import datetime +from sqlalchemy import and_, or_, select +from sqlalchemy.schema import Table +from sqlalchemy.sql.expression import Select +from sqlalchemy.sql import func + + +from src.utils import UserContext + +from .authorization import Authorization, AuthType from .base import BaseModel from .constants import WebApiTables @@ -13,13 +22,27 @@ class FormProcessMapper(BaseModel): _table = None # cache for the mapper table @classmethod - async def first(cls, **filters): - return await super().first(**filters) + async def first( + cls, + user_context: UserContext = None, + **filters + ): + table = await cls.get_table() + query = await super().first(**filters) + if user_context: + query = await cls.apply_tenant_auth(query, table, user_context) + return query @classmethod - async def find_all(cls, **filters): - query = await super().find_all(**filters) + async def find_all( + cls, + user_context: UserContext = None, + **filters + ): table = await cls.get_table() + query = await super().find_all(**filters) + if user_context: + query = await cls.apply_tenant_auth(query, table, user_context) # Apply date filters, if any if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): @@ -29,4 +52,34 @@ async def find_all(cls, **filters): if to_date := filters.get("to_date"): query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) - return query + # Get total count + count = (await cls.execute(select(func.count()).select_from(query.subquery()))).scalar_one() + return query, count + + @classmethod + async def apply_tenant_auth( + cls, + query: Select, + table: Table, + user_context: UserContext + ): + """Takes a SQLAlchemy Select query and applies additional tenant auth checks.""" + # Parse user context info + tenant_key = user_context.tenant_key + user_groups = user_context.token_info.get("groups", []) + + # Build role conditions array + auth_table = await Authorization.get_table() + role_conditions = await Authorization.get_role_conditions(auth_table, user_groups) + + # Apply tenant auth checks + query = query.join( + auth_table, + and_( + table.c.parent_form_id == auth_table.c.resource_id, + auth_table.c.tenant == tenant_key, + or_(*role_conditions), # ⬅️ Role conditions + auth_table.c.auth_type == AuthType.APPLICATION.value, + ), + ) + return query \ No newline at end of file From 344636c1ae95d9fb67d4e7235d97550ef90d33d6 Mon Sep 17 00:00:00 2001 From: leodube-aot Date: Tue, 5 Aug 2025 09:34:24 -0400 Subject: [PATCH 16/16] Final cleanup --- forms-flow-data-layer/src/middlewares/auth.py | 2 - .../src/models/webapi/application.py | 57 --- forms-flow-web/package-lock.json | 476 ------------------ forms-flow-web/package.json | 1 - .../src/components/Dashboard/Dashboard.js | 12 +- .../src/components/Dashboard/StatusChart.js | 2 +- 6 files changed, 7 insertions(+), 543 deletions(-) diff --git a/forms-flow-data-layer/src/middlewares/auth.py b/forms-flow-data-layer/src/middlewares/auth.py index 366935ca59..f14331e03b 100644 --- a/forms-flow-data-layer/src/middlewares/auth.py +++ b/forms-flow-data-layer/src/middlewares/auth.py @@ -36,8 +36,6 @@ async def has_permission( token = auth.split(" ")[1] payload = await keycloak_validator.verify_token(token) # Attach token info to context - print('token', token) - print('payload', payload) info.context["user"] = UserContext(token=token, token_info=payload) return True except Exception as e: diff --git a/forms-flow-data-layer/src/models/webapi/application.py b/forms-flow-data-layer/src/models/webapi/application.py index 21c35f09d0..9d2d9fa997 100644 --- a/forms-flow-data-layer/src/models/webapi/application.py +++ b/forms-flow-data-layer/src/models/webapi/application.py @@ -66,63 +66,6 @@ def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): limit = 5 return query.limit(limit).offset((page_no - 1) * limit) - @classmethod - - async def find_aggregated_application_metrics(cls, metric: str, **filters): - """Fetch application metrics.""" - table = await cls.get_table() - - if not hasattr(table.c, metric): - return None - - metric_col = getattr(table.c, metric) - query = select( - metric_col.label("metric"), - func.count(table.c.id).label("count") - ).group_by(metric_col) - - # Apply filters - for key, value in filters.items(): - if hasattr(table.c, key): - query = query.where(getattr(table.c, key) == value) - - # Apply date filters, if any - if (order_by := filters.get("order_by")) and hasattr(table.c, order_by): - if from_date := filters.get("from_date"): - query = query.where(getattr(table.c, order_by) >= datetime.fromisoformat(from_date)) - if to_date := filters.get("to_date"): - query = query.where(getattr(table.c, order_by) <= datetime.fromisoformat(to_date)) - - return query - - - @classmethod - def filter_query(cls, query, filter: dict, application_table): - """ - Apply filters to the SQLAlchemy query. - """ - for field, value in filter.items(): - if hasattr(application_table.c, field): - col = getattr(application_table.c, field) - if field == "id": - # Special case for application_id - query = query.where(col == value) - else: - # For other fields, use ilike for case-insensitive search - query = query.where(col.ilike(f"%{value}%")) - return query - - @classmethod - def paginationed_query(cls, query, page_no: int = 1, limit: int = 5): - """ - Paginate the SQLAlchemy query. - """ - if page_no < 1: - page_no = 1 - if limit < 1: - limit = 5 - return query.limit(limit).offset((page_no - 1) * limit) - @classmethod async def get_authorized_applications( cls, diff --git a/forms-flow-web/package-lock.json b/forms-flow-web/package-lock.json index e0dab0f600..feeaf86305 100644 --- a/forms-flow-web/package-lock.json +++ b/forms-flow-web/package-lock.json @@ -68,7 +68,6 @@ "react-scripts": "^5.0.1", "react-select": "^3.2.0", "react-toastify": "^7.0.4", - "recharts": "^2.12.7", "redux": "^4.1.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", @@ -4777,60 +4776,6 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -8287,116 +8232,6 @@ "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8518,11 +8353,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -10311,14 +10141,6 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -11953,14 +11775,6 @@ "node": ">= 0.4" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -17708,20 +17522,6 @@ "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-toastify": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.4.tgz", @@ -17781,49 +17581,6 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -20875,27 +20632,6 @@ "node": ">= 0.8" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -25029,60 +24765,6 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, - "@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" - }, - "@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "requires": { - "@types/d3-color": "*" - } - }, - "@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "requires": { - "@types/d3-time": "*" - } - }, - "@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "requires": { - "@types/d3-path": "*" - } - }, - "@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, "@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -27724,83 +27406,6 @@ "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" }, - "d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "requires": { - "internmap": "1 - 2" - } - }, - "d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "requires": { - "d3-color": "1 - 3" - } - }, - "d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" - }, - "d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "requires": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - } - }, - "d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "requires": { - "d3-path": "^3.1.0" - } - }, - "d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "requires": { - "d3-array": "2 - 3" - } - }, - "d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "requires": { - "d3-time": "1 - 3" - } - }, - "d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -27885,11 +27490,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, - "decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -29274,11 +28874,6 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, - "fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==" - }, "fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -30445,11 +30040,6 @@ "side-channel": "^1.1.0" } }, - "internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -34469,16 +34059,6 @@ "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", "requires": {} }, - "react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "requires": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - } - }, "react-toastify": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-7.0.4.tgz", @@ -34524,41 +34104,6 @@ "picomatch": "^2.2.1" } }, - "recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "requires": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "dependencies": { - "clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - } - } - }, - "recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "requires": { - "decimal.js-light": "^2.4.1" - } - }, "recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -36831,27 +36376,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "requires": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/forms-flow-web/package.json b/forms-flow-web/package.json index 0851682995..43ad996371 100644 --- a/forms-flow-web/package.json +++ b/forms-flow-web/package.json @@ -99,7 +99,6 @@ "react-scripts": "^5.0.1", "react-select": "^3.2.0", "react-toastify": "^7.0.4", - "recharts": "^2.12.7", "redux": "^4.1.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", diff --git a/forms-flow-web/src/components/Dashboard/Dashboard.js b/forms-flow-web/src/components/Dashboard/Dashboard.js index 1eedfc3c66..f84bb5689a 100644 --- a/forms-flow-web/src/components/Dashboard/Dashboard.js +++ b/forms-flow-web/src/components/Dashboard/Dashboard.js @@ -355,12 +355,12 @@ const Dashboard = React.memo(() => { - + )} diff --git a/forms-flow-web/src/components/Dashboard/StatusChart.js b/forms-flow-web/src/components/Dashboard/StatusChart.js index f6b7e308b6..e98bcf939e 100644 --- a/forms-flow-web/src/components/Dashboard/StatusChart.js +++ b/forms-flow-web/src/components/Dashboard/StatusChart.js @@ -86,7 +86,7 @@ const ChartForm = React.memo((props) => { borderColor: BORDER_COLORS, borderWidth: 1 }] - } || {}; + }; const chartOptions = { plugins: {