From bfee266d07f49b7454ea15e897534d69cfc5a902 Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Mon, 2 Sep 2024 15:51:10 +0200 Subject: [PATCH 1/4] add project filter on form stats page --- .../js/apps/Iaso/components/instancesGraph.js | 18 ++-- .../Iaso/components/instancesTotalGraph.js | 19 ++-- hat/assets/js/apps/Iaso/constants/urls.ts | 10 +- .../forms/components/formStasts/Filters.tsx | 100 ++++++++++++++++++ .../domains/forms/hooks/UseGetFormStats.tsx | 42 ++++++++ .../js/apps/Iaso/domains/forms/stats.js | 47 ++++++-- iaso/api/instances.py | 12 +++ 7 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 hat/assets/js/apps/Iaso/domains/forms/components/formStasts/Filters.tsx create mode 100644 hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx diff --git a/hat/assets/js/apps/Iaso/components/instancesGraph.js b/hat/assets/js/apps/Iaso/components/instancesGraph.js index 1ad373febf..3dd5efba7e 100644 --- a/hat/assets/js/apps/Iaso/components/instancesGraph.js +++ b/hat/assets/js/apps/Iaso/components/instancesGraph.js @@ -12,14 +12,11 @@ import { } from 'recharts'; import { Typography } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { getRequest } from 'Iaso/libs/Api'; -import { useSnackQuery } from 'Iaso/libs/apiHooks'; +import PropTypes from 'prop-types'; import { getChipColors } from '../constants/chipColors'; -export const InstancesPerFormGraph = () => { - const { data, isLoading } = useSnackQuery(['instances', 'stats'], () => - getRequest('/api/instances/stats/'), - ); +export const InstancesPerFormGraph = props => { + const { data, isLoading } = props; return ( <> @@ -38,7 +35,7 @@ export const InstancesPerFormGraph = () => { data={data.data} margin={{ top: 20, - right: 30, + right: 0, left: 20, bottom: 5, }} @@ -64,3 +61,10 @@ export const InstancesPerFormGraph = () => { ); }; +InstancesPerFormGraph.defaultProps = { + data: null, +}; +InstancesPerFormGraph.propTypes = { + data: PropTypes.object, + isLoading: PropTypes.bool.isRequired, +}; diff --git a/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js b/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js index 9e3cc72a24..cd8a655595 100644 --- a/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js +++ b/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js @@ -11,14 +11,10 @@ import { import { Typography } from '@mui/material'; import { LoadingSpinner } from 'bluesquare-components'; import { FormattedMessage } from 'react-intl'; -import { useSnackQuery } from 'Iaso/libs/apiHooks'; -import { getRequest } from 'Iaso/libs/Api'; - -export const InstancesTotalGraph = () => { - const { data, isLoading } = useSnackQuery(['instances', 'stats_sum'], () => - getRequest('/api/instances/stats_sum/'), - ); +import PropTypes from 'prop-types'; +export const InstancesTotalGraph = props => { + const { data, isLoading } = props; return ( <> @@ -51,3 +47,12 @@ export const InstancesTotalGraph = () => { ); }; + +InstancesTotalGraph.defaultProps = { + data: null, +}; + +InstancesTotalGraph.propTypes = { + data: PropTypes.object, + isLoading: PropTypes.bool.isRequired, +}; diff --git a/hat/assets/js/apps/Iaso/constants/urls.ts b/hat/assets/js/apps/Iaso/constants/urls.ts index 5fcda03c5b..3bab09452d 100644 --- a/hat/assets/js/apps/Iaso/constants/urls.ts +++ b/hat/assets/js/apps/Iaso/constants/urls.ts @@ -70,7 +70,7 @@ export const baseRouteConfigs: Record = { ...paginationPathParamsWithPrefix('attachments'), ], }, - formsStats: { url: 'forms/stats', params: ['accountId'] }, + formsStats: { url: 'forms/stats', params: ['accountId', 'projectIds'] }, instances: { url: 'forms/submissions', params: [ @@ -120,13 +120,13 @@ export const baseRouteConfigs: Record = { mappings: { url: 'forms/mappings', params: [ - 'accountId', - 'formId', + 'accountId', + 'formId', 'mappingTypes', 'orgUnitTypeIds', 'projectsIds', - 'search', - ...paginationPathParams + 'search', + ...paginationPathParams, ], }, mappingDetail: { diff --git a/hat/assets/js/apps/Iaso/domains/forms/components/formStasts/Filters.tsx b/hat/assets/js/apps/Iaso/domains/forms/components/formStasts/Filters.tsx new file mode 100644 index 0000000000..52b1d6de34 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/forms/components/formStasts/Filters.tsx @@ -0,0 +1,100 @@ +import { Button, Grid } from '@mui/material'; +import React, { FunctionComponent, useCallback, useState } from 'react'; +import SearchIcon from '@mui/icons-material/Search'; +import { makeStyles } from '@mui/styles'; +import { + commonStyles, + useRedirectTo, + useSafeIntl, +} from 'bluesquare-components'; +import InputComponent from '../../../../components/forms/InputComponent'; +import MESSAGES from '../../messages'; +import { useGetProjectsDropdownOptions } from '../../../projects/hooks/requests'; + +const useStyles = makeStyles(theme => ({ + ...commonStyles(theme), +})); + +type Params = { + projectIds?: string; +}; + +type Props = { + baseUrl: string; + params: Params; +}; + +export const Filters: FunctionComponent = ({ baseUrl, params }) => { + const [filtersUpdated, setFiltersUpdated] = useState(false); + const classes = useStyles(); + const { formatMessage } = useSafeIntl(); + const { data: allProjects, isFetching: isFetchingProjects } = + useGetProjectsDropdownOptions(); + const [filters, setFilters] = useState({ + projectIds: params.projectIds, + }); + + const handleChange = useCallback( + (key, value) => { + setFiltersUpdated(true); + setFilters({ + ...filters, + [key]: value, + }); + }, + [filters], + ); + + const redirectTo = useRedirectTo(); + const handleSearch = useCallback(() => { + if (filtersUpdated) { + setFiltersUpdated(false); + const tempParams = { + ...params, + ...filters, + }; + redirectTo(baseUrl, tempParams); + } + }, [filtersUpdated, params, filters, redirectTo, baseUrl]); + + return ( + + + + + + + + + + ); +}; diff --git a/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx b/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx new file mode 100644 index 0000000000..041baee180 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx @@ -0,0 +1,42 @@ +import { UseQueryResult } from 'react-query'; +import { getRequest } from '../../../libs/Api'; +import { useSnackQuery } from '../../../libs/apiHooks'; +import { useParamsObject } from '../../../routing/hooks/useParamsObject'; +import { makeUrlWithParams } from '../../../libs/utils'; + +const getFormStats = url => { + return getRequest(url); +}; + +type ApiParams = { + project_ids?: string; +}; + +const useGetApiParams = (params: { + accountId: string; + projectIds: string; +}): ApiParams => ({ + project_ids: params.projectIds, +}); + +export const useGetFormStats = ( + baseUrl, + url, + queryKey, +): UseQueryResult => { + const params = useParamsObject(baseUrl) as { + accountId: string; + projectIds: string; + }; + const apiParams = useGetApiParams(params); + const apiUrl = makeUrlWithParams(url, apiParams); + return useSnackQuery({ + queryKey: [queryKey, params], + queryFn: () => getFormStats(apiUrl), + options: { + staleTime: 60000, + cacheTime: 60000, + keepPreviousData: true, + }, + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/forms/stats.js b/hat/assets/js/apps/Iaso/domains/forms/stats.js index eb8cf61c1a..96f63f8273 100644 --- a/hat/assets/js/apps/Iaso/domains/forms/stats.js +++ b/hat/assets/js/apps/Iaso/domains/forms/stats.js @@ -6,6 +6,12 @@ import TopBar from '../../components/nav/TopBarComponent'; import { InstancesPerFormGraph } from '../../components/instancesGraph'; import MESSAGES from './messages'; import { InstancesTotalGraph } from '../../components/instancesTotalGraph'; +import { Filters } from './components/formStasts/Filters.tsx'; +import { baseUrls } from '../../constants/urls.ts'; +import { useParamsObject } from '../../routing/hooks/useParamsObject.tsx'; +import { useGetFormStats } from './hooks/UseGetFormStats.tsx'; + +const baseUrl = baseUrls.formsStats; const useStyles = makeStyles(theme => ({ ...commonStyles(theme), @@ -18,17 +24,46 @@ const useStyles = makeStyles(theme => ({ const FormsStats = () => { const classes = useStyles(); const { formatMessage } = useSafeIntl(); + const params = useParamsObject(baseUrl); + const { data: dataStats, isLoading: isLoadingDataStats } = useGetFormStats( + baseUrl, + '/api/instances/stats/', + ['instances', 'stats'], + ); + + const { data: dataStatsSum, isLoading: isLoadingDataStatsSum } = + useGetFormStats(baseUrl, '/api/instances/stats_sum/', [ + 'instances', + 'stats_sum', + ]); return ( <> - + + - - - + + + + + - - + + + + + + + diff --git a/iaso/api/instances.py b/iaso/api/instances.py index e1ffdc612f..2ea24cbcfc 100644 --- a/iaso/api/instances.py +++ b/iaso/api/instances.py @@ -593,7 +593,13 @@ def bulkdelete(self, request): @action(detail=False) def stats(self, request): + project_ids_param = request.GET.get("project_ids", None) projects = request.user.iaso_profile.account.project_set.all() + + if project_ids_param: + project_ids_array = project_ids_param.split(",") + projects = projects.filter(id__in=project_ids_array) + projects_ids = list(projects.values_list("id", flat=True)) df = pd.read_sql_query(self.QUERY, connection, params=[projects_ids]) @@ -614,7 +620,13 @@ def stats(self, request): @action(detail=False) def stats_sum(self, request): + project_ids_param = request.GET.get("project_ids", None) projects = request.user.iaso_profile.account.project_set.all() + + if project_ids_param: + project_ids_array = project_ids_param.split(",") + projects = projects.filter(id__in=project_ids_array) + projects_ids = list(projects.values_list("id", flat=True)) QUERY = """ select DATE_TRUNC('day', COALESCE(iaso_instance.source_created_at, iaso_instance.created_at)) as period, From 4a451daac1ace03e51971451186fa9b4cc61b483 Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Mon, 2 Sep 2024 16:46:22 +0200 Subject: [PATCH 2/4] refactor some js files into typescript --- .../{instancesGraph.js => instancesGraph.tsx} | 40 +++++++++++----- ...sTotalGraph.js => instancesTotalGraph.tsx} | 46 +++++++++++++------ hat/assets/js/apps/Iaso/constants/routes.tsx | 2 +- .../domains/forms/{stats.js => stats.tsx} | 19 ++++---- 4 files changed, 73 insertions(+), 34 deletions(-) rename hat/assets/js/apps/Iaso/components/{instancesGraph.js => instancesGraph.tsx} (79%) rename hat/assets/js/apps/Iaso/components/{instancesTotalGraph.js => instancesTotalGraph.tsx} (73%) rename hat/assets/js/apps/Iaso/domains/forms/{stats.js => stats.tsx} (83%) diff --git a/hat/assets/js/apps/Iaso/components/instancesGraph.js b/hat/assets/js/apps/Iaso/components/instancesGraph.tsx similarity index 79% rename from hat/assets/js/apps/Iaso/components/instancesGraph.js rename to hat/assets/js/apps/Iaso/components/instancesGraph.tsx index 3dd5efba7e..7fd7b19d70 100644 --- a/hat/assets/js/apps/Iaso/components/instancesGraph.js +++ b/hat/assets/js/apps/Iaso/components/instancesGraph.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { LoadingSpinner } from 'bluesquare-components'; import { Bar, @@ -12,12 +12,37 @@ import { } from 'recharts'; import { Typography } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; import { getChipColors } from '../constants/chipColors'; -export const InstancesPerFormGraph = props => { - const { data, isLoading } = props; +type Data = { + index: string; + name: string; + [key: string]: string | number | null; +}[]; +type Field = { + name: string; + type: string; + freq?: string; +}; + +type Schema = { + fields: Field[]; + pandas_version: string; + primaryKey: string[]; +}; + +type Props = { + data: { + data: Data; + schema: Schema; + }; + isLoading: boolean; +}; +export const InstancesPerFormGraph: FunctionComponent = ({ + data, + isLoading, +}) => { return ( <> @@ -61,10 +86,3 @@ export const InstancesPerFormGraph = props => { ); }; -InstancesPerFormGraph.defaultProps = { - data: null, -}; -InstancesPerFormGraph.propTypes = { - data: PropTypes.object, - isLoading: PropTypes.bool.isRequired, -}; diff --git a/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js b/hat/assets/js/apps/Iaso/components/instancesTotalGraph.tsx similarity index 73% rename from hat/assets/js/apps/Iaso/components/instancesTotalGraph.js rename to hat/assets/js/apps/Iaso/components/instancesTotalGraph.tsx index cd8a655595..6d9db28c56 100644 --- a/hat/assets/js/apps/Iaso/components/instancesTotalGraph.js +++ b/hat/assets/js/apps/Iaso/components/instancesTotalGraph.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { CartesianGrid, Line, @@ -11,10 +11,39 @@ import { import { Typography } from '@mui/material'; import { LoadingSpinner } from 'bluesquare-components'; import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; -export const InstancesTotalGraph = props => { - const { data, isLoading } = props; +type Data = { + index: number; + period: string; + value: number; + total: number; + name: string; +}[]; + +type Field = { + name: string; + type: string; + tz?: string; +}; + +type Schema = { + fields: Field[]; + pandas_version: string; + primaryKey: string[]; +}; + +type Props = { + data: { + data: Data; + schema: Schema; + }; + isLoading: boolean; +}; + +export const InstancesTotalGraph: FunctionComponent = ({ + data, + isLoading, +}) => { return ( <> @@ -47,12 +76,3 @@ export const InstancesTotalGraph = props => { ); }; - -InstancesTotalGraph.defaultProps = { - data: null, -}; - -InstancesTotalGraph.propTypes = { - data: PropTypes.object, - isLoading: PropTypes.bool.isRequired, -}; diff --git a/hat/assets/js/apps/Iaso/constants/routes.tsx b/hat/assets/js/apps/Iaso/constants/routes.tsx index 1cf7c252be..1d51c0f876 100644 --- a/hat/assets/js/apps/Iaso/constants/routes.tsx +++ b/hat/assets/js/apps/Iaso/constants/routes.tsx @@ -15,7 +15,7 @@ import { Duplicates } from '../domains/entities/duplicates/list/Duplicates'; import { EntityTypes } from '../domains/entities/entityTypes'; import Forms from '../domains/forms'; import FormDetail from '../domains/forms/detail'; -import FormsStats from '../domains/forms/stats'; +import { FormsStats } from '../domains/forms/stats'; import Instances from '../domains/instances'; import CompareSubmissions from '../domains/instances/compare'; import { CompareInstanceLogs } from '../domains/instances/compare/components/CompareInstanceLogs'; diff --git a/hat/assets/js/apps/Iaso/domains/forms/stats.js b/hat/assets/js/apps/Iaso/domains/forms/stats.tsx similarity index 83% rename from hat/assets/js/apps/Iaso/domains/forms/stats.js rename to hat/assets/js/apps/Iaso/domains/forms/stats.tsx index 96f63f8273..e4c709daef 100644 --- a/hat/assets/js/apps/Iaso/domains/forms/stats.js +++ b/hat/assets/js/apps/Iaso/domains/forms/stats.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { Box, Grid } from '@mui/material'; import { commonStyles, useSafeIntl } from 'bluesquare-components'; import { makeStyles } from '@mui/styles'; @@ -6,10 +6,10 @@ import TopBar from '../../components/nav/TopBarComponent'; import { InstancesPerFormGraph } from '../../components/instancesGraph'; import MESSAGES from './messages'; import { InstancesTotalGraph } from '../../components/instancesTotalGraph'; -import { Filters } from './components/formStasts/Filters.tsx'; -import { baseUrls } from '../../constants/urls.ts'; -import { useParamsObject } from '../../routing/hooks/useParamsObject.tsx'; -import { useGetFormStats } from './hooks/UseGetFormStats.tsx'; +import { Filters } from './components/formStasts/Filters'; +import { baseUrls } from '../../constants/urls'; +import { useParamsObject } from '../../routing/hooks/useParamsObject'; +import { useGetFormStats } from './hooks/UseGetFormStats'; const baseUrl = baseUrls.formsStats; @@ -21,10 +21,12 @@ const useStyles = makeStyles(theme => ({ }, })); -const FormsStats = () => { - const classes = useStyles(); +type Params = { accountId: string; projectIds?: string }; + +export const FormsStats: FunctionComponent = () => { + const classes: Record = useStyles(); const { formatMessage } = useSafeIntl(); - const params = useParamsObject(baseUrl); + const params: Params = useParamsObject(baseUrl); const { data: dataStats, isLoading: isLoadingDataStats } = useGetFormStats( baseUrl, '/api/instances/stats/', @@ -70,4 +72,3 @@ const FormsStats = () => { ); }; -export default FormsStats; From cb6d9b5da6278f97d6d263d448fdafa57b9600bd Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Tue, 3 Sep 2024 13:06:01 +0200 Subject: [PATCH 3/4] add form stats tests --- iaso/tests/api/test_instances.py | 95 ++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/iaso/tests/api/test_instances.py b/iaso/tests/api/test_instances.py index 3cf429fc5d..2a82f141bc 100644 --- a/iaso/tests/api/test_instances.py +++ b/iaso/tests/api/test_instances.py @@ -82,10 +82,12 @@ def setUpTestData(cls): name="Hydroponic gardens", app_id="stars.empire.agriculture.hydroponics", account=star_wars ) + cls.project_2 = m.Project.objects.create(name="Project number 2", app_id="project.two", account=star_wars) + sw_source.projects.add(cls.project) cls.form_1 = m.Form.objects.create(name="Hydroponics study", period_type=m.MONTH, single_per_period=True) - + cls.form_5 = m.Form.objects.create(name="Form five", period_type=m.MONTH, single_per_period=True) date1 = datetime.datetime(2020, 2, 1, 0, 0, 5, tzinfo=pytz.UTC) date2 = datetime.datetime(2020, 2, 3, 0, 0, 5, tzinfo=pytz.UTC) date3 = datetime.datetime(2020, 2, 5, 0, 0, 5, tzinfo=pytz.UTC) @@ -190,6 +192,15 @@ def setUpTestData(cls): ) cls.form_4.save() + with patch("django.utils.timezone.now", lambda: date3): + cls.instance_8 = cls.create_form_instance( + form=cls.form_5, + period="202003", + org_unit=cls.jedi_council_corruscant, + project=cls.project_2, + created_by=cls.yoda, + source_created_at=date3, + ) with patch("django.utils.timezone.now", lambda: datetime.datetime(2020, 2, 10, 0, 0, 5, tzinfo=pytz.UTC)): cls.instance_5.save() cls.instance_6.save() @@ -497,7 +508,7 @@ def test_instance_filter_by_org_unit_status(self): response = self.client.get(f"/api/instances/?org_unit_status=VALID") self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 6) + self.assertValidInstanceListData(response.json(), 7) response = self.client.get(f"/api/instances/?org_unit_status=REJECTED") self.assertJSONResponse(response, 200) @@ -744,7 +755,7 @@ def test_instance_list_by_search_org_unit_ref(self): response = self.client.get(f"/api/instances/", {"search": "refs:" + self.jedi_council_corruscant.source_ref}) self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 6) + self.assertValidInstanceListData(response.json(), 7) def test_instance_list_by_search_org_unit_ref_not_found(self): """GET /instances/?search=refs:org_unit__source_ref""" @@ -1054,7 +1065,7 @@ def test_user_restriction(self): # not restricted yet, can list all instances response = self.client.get(f"/api/instances/") self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 9) + self.assertValidInstanceListData(response.json(), 10) # restrict user to endor region, can only see one instance. Not instance without org unit restricted = self.create_user_with_profile( username="restricted", account=self.star_wars, permissions=["iaso_submissions"] @@ -1083,7 +1094,7 @@ def test_user_restriction(self): response = self.client.get(f"/api/instances/") self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 7) + self.assertValidInstanceListData(response.json(), 8) # Check org unit without submissions return empty restricted.iaso_profile.org_units.set([org_unit_without_submissions]) @@ -1102,6 +1113,7 @@ def test_stats(self): r["data"], [ { + "Form five": 1, "Hydroponic public survey": 1, "Hydroponic public survey III": 1, "Hydroponics study": 4, @@ -1116,24 +1128,46 @@ def test_stats(self): [ {"freq": "M", "name": "index", "type": "datetime"}, {"name": "Hydroponics study", "type": "integer"}, + {"name": "Form five", "type": "integer"}, {"name": "Hydroponic public survey", "type": "integer"}, {"name": "Hydroponic public survey III", "type": "integer"}, {"name": "name", "type": "string"}, ], ) + @mock.patch("django.utils.timezone.now", lambda: MOCK_DATE) + def test_stats_project_filter(self): + self.client.force_authenticate(self.yoda) + response = self.client.get(f"/api/instances/stats/?project_ids={self.project_2.id}") + r = self.assertJSONResponse(response, 200) + + self.assertEqual( + r["data"], + [{"index": "2020-02-01T00:00:00.000", "Form five": 1, "name": "2020-02"}], + ) + + self.assertListEqual( + r["schema"]["fields"], + [ + {"name": "index", "type": "datetime", "freq": "M"}, + {"name": "Form five", "type": "integer"}, + {"name": "name", "type": "string"}, + ], + ) + @mock.patch("django.utils.timezone.now", lambda: MOCK_DATE) def test_stats_sum(self): self.client.force_authenticate(self.yoda) response = self.client.get(f"/api/instances/stats_sum/") r = self.assertJSONResponse(response, 200) + self.assertEqual( r["data"], [ - {"index": 0, "name": "2020-02-01", "period": "2020-02-01T00:00:00.000Z", "total": 2, "value": 2}, - {"index": 1, "name": "2020-02-02", "period": "2020-02-02T00:00:00.000Z", "total": 4, "value": 2}, - {"index": 2, "name": "2020-02-03", "period": "2020-02-03T00:00:00.000Z", "total": 5, "value": 1}, - {"index": 3, "name": "2020-02-05", "period": "2020-02-05T00:00:00.000Z", "total": 6, "value": 1}, + {"index": 0, "period": "2020-02-01T00:00:00.000Z", "value": 2, "total": 2, "name": "2020-02-01"}, + {"index": 1, "period": "2020-02-02T00:00:00.000Z", "value": 2, "total": 4, "name": "2020-02-02"}, + {"index": 2, "period": "2020-02-03T00:00:00.000Z", "value": 1, "total": 5, "name": "2020-02-03"}, + {"index": 3, "period": "2020-02-05T00:00:00.000Z", "value": 2, "total": 7, "name": "2020-02-05"}, ], ) @@ -1148,6 +1182,27 @@ def test_stats_sum(self): ], ) + @mock.patch("django.utils.timezone.now", lambda: MOCK_DATE) + def test_stats_sum_project_filter(self): + self.client.force_authenticate(self.yoda) + response = self.client.get(f"/api/instances/stats_sum/?project_ids={self.project_2.id}") + r = self.assertJSONResponse(response, 200) + self.assertEqual( + r["data"], + [{"index": 0, "period": "2020-02-05T00:00:00.000Z", "value": 1, "total": 1, "name": "2020-02-05"}], + ) + + self.assertEqual( + r["schema"]["fields"], + [ + {"name": "index", "type": "integer"}, + {"name": "period", "type": "datetime", "tz": "UTC"}, + {"name": "value", "type": "integer"}, + {"name": "total", "type": "integer"}, + {"name": "name", "type": "string"}, + ], + ) + @mock.patch("django.utils.timezone.now", lambda: MOCK_DATE) def test_stats_dup(self): """Fix for regression on IA-940 endpoint was failing due to duplicate form name""" @@ -1186,6 +1241,7 @@ def test_stats_dup(self): [ { "Duplicate form": 2, + "Form five": 1, "Hydroponic public survey": 1, "Hydroponic public survey III": 1, "Hydroponics study": 4, @@ -1236,6 +1292,7 @@ def test_stats_dup_deleted(self): [ { "Duplicate form": 1, + "Form five": 1, "Hydroponic public survey": 1, "Hydroponic public survey III": 1, "Hydroponics study": 4, @@ -1574,7 +1631,7 @@ def test_instances_list_planning(self): self.client.force_authenticate(self.yoda) response = self.client.get("/api/instances/", headers={"Content-Type": "application/json"}) self.assertEqual(response.status_code, 200) - self.assertValidInstanceListData(response.json(), 6) + self.assertValidInstanceListData(response.json(), 7) team = Team.objects.create(project=self.project, manager=self.yoda) orgunit = m.OrgUnit.objects.create(name="Org Unit 1") @@ -1602,7 +1659,7 @@ def test_instances_list_planning(self): self.client.force_authenticate(self.yoda) response = self.client.get("/api/instances/?order=-id", headers={"Content-Type": "application/json"}) self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 8) + self.assertValidInstanceListData(response.json(), 9) self.assertEqual(response.json()["instances"][0]["id"], instance_2.id) self.assertEqual(response.json()["instances"][1]["id"], instance_1.id) @@ -1622,11 +1679,11 @@ def test_instances_list_user(self): headers={"Content-Type": "application/json"}, ) self.assertEqual(response_yoda.status_code, 200) - self.assertValidInstanceListData(response_yoda.json(), 3) + self.assertValidInstanceListData(response_yoda.json(), 4) instances = response_yoda.json()["instances"] self.assertEqual(self.instance_1.id, instances[0].get("id")) self.assertEqual(self.instance_4.id, instances[1].get("id")) - self.assertEqual(self.instance_5.id, instances[2].get("id")) + self.assertEqual(self.instance_5.id, instances[3].get("id")) self.client.force_authenticate(self.yoda) response_yoda_deleted = self.client.get( @@ -1666,12 +1723,13 @@ def test_instances_list_user(self): headers={"Content-Type": "application/json"}, ) self.assertEqual(response_yoda_guest.status_code, 200) - self.assertValidInstanceListData(response_yoda_guest.json(), 4) + self.assertValidInstanceListData(response_yoda_guest.json(), 5) instances = response_yoda_guest.json()["instances"] + self.assertEqual(self.instance_1.id, instances[0].get("id")) self.assertEqual(self.instance_4.id, instances[1].get("id")) - self.assertEqual(self.instance_5.id, instances[2].get("id")) - self.assertEqual(self.instance_2.id, instances[3].get("id")) + self.assertEqual(self.instance_8.id, instances[2].get("id")) + self.assertEqual(self.instance_2.id, instances[4].get("id")) def test_instances_bad_sent_date_from(self): self.client.force_authenticate(self.yoda) @@ -1729,7 +1787,7 @@ def test_instances_sent_date(self): headers={"Content-Type": "application/json"}, ) self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 1) + self.assertValidInstanceListData(response.json(), 2) instances = response.json()["instances"] self.assertEqual(self.instance_4.id, instances[0].get("id")) @@ -1788,7 +1846,7 @@ def test_instances_filter_from_date_to_date(self): headers={"Content-Type": "application/json"}, ) self.assertJSONResponse(response, 200) - self.assertValidInstanceListData(response.json(), 3) + self.assertValidInstanceListData(response.json(), 4) instances = response.json()["instances"] instance_ids = [i["id"] for i in instances] instance_ids.sort() @@ -1797,6 +1855,7 @@ def test_instances_filter_from_date_to_date(self): [ self.instance_3.id, self.instance_4.id, + self.instance_8.id, another_instance.id, ], ) From d3bf62408b9b7bf2762f11f26190f5d23e989bfb Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Wed, 4 Sep 2024 15:08:11 +0200 Subject: [PATCH 4/4] fix style and refactor the code to be more clear --- .../domains/forms/hooks/UseGetFormStats.tsx | 43 ++++++++++--------- .../js/apps/Iaso/domains/forms/stats.tsx | 28 ++++++------ .../Iaso/domains/forms/types/formStats.ts | 7 +++ 3 files changed, 41 insertions(+), 37 deletions(-) create mode 100644 hat/assets/js/apps/Iaso/domains/forms/types/formStats.ts diff --git a/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx b/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx index 041baee180..e8beb8fa73 100644 --- a/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx +++ b/hat/assets/js/apps/Iaso/domains/forms/hooks/UseGetFormStats.tsx @@ -1,37 +1,22 @@ import { UseQueryResult } from 'react-query'; import { getRequest } from '../../../libs/Api'; import { useSnackQuery } from '../../../libs/apiHooks'; -import { useParamsObject } from '../../../routing/hooks/useParamsObject'; import { makeUrlWithParams } from '../../../libs/utils'; +import { ApiFormStatsParams } from '../types/formStats'; const getFormStats = url => { return getRequest(url); }; -type ApiParams = { - project_ids?: string; -}; - -const useGetApiParams = (params: { - accountId: string; - projectIds: string; -}): ApiParams => ({ - project_ids: params.projectIds, -}); - -export const useGetFormStats = ( - baseUrl, +const useGetFormStats = ({ + params, url, queryKey, -): UseQueryResult => { - const params = useParamsObject(baseUrl) as { - accountId: string; - projectIds: string; - }; - const apiParams = useGetApiParams(params); +}: ApiFormStatsParams): UseQueryResult => { + const apiParams = { project_ids: params.projectIds }; const apiUrl = makeUrlWithParams(url, apiParams); return useSnackQuery({ - queryKey: [queryKey, params], + queryKey: [queryKey, apiUrl], queryFn: () => getFormStats(apiUrl), options: { staleTime: 60000, @@ -40,3 +25,19 @@ export const useGetFormStats = ( }, }); }; + +export const useGetPerFormStats = params => { + return useGetFormStats({ + params, + url: '/api/instances/stats/', + queryKey: ['instances', 'stats'], + }); +}; + +export const useGetFormStatsSum = params => { + return useGetFormStats({ + params, + url: '/api/instances/stats_sum/', + queryKey: ['instances', 'stats_sum'], + }); +}; diff --git a/hat/assets/js/apps/Iaso/domains/forms/stats.tsx b/hat/assets/js/apps/Iaso/domains/forms/stats.tsx index e4c709daef..321b123d1e 100644 --- a/hat/assets/js/apps/Iaso/domains/forms/stats.tsx +++ b/hat/assets/js/apps/Iaso/domains/forms/stats.tsx @@ -9,7 +9,11 @@ import { InstancesTotalGraph } from '../../components/instancesTotalGraph'; import { Filters } from './components/formStasts/Filters'; import { baseUrls } from '../../constants/urls'; import { useParamsObject } from '../../routing/hooks/useParamsObject'; -import { useGetFormStats } from './hooks/UseGetFormStats'; +import { + useGetPerFormStats, + useGetFormStatsSum, +} from './hooks/UseGetFormStats'; +import { FormStatsParams } from './types/formStats'; const baseUrl = baseUrls.formsStats; @@ -21,23 +25,15 @@ const useStyles = makeStyles(theme => ({ }, })); -type Params = { accountId: string; projectIds?: string }; - export const FormsStats: FunctionComponent = () => { const classes: Record = useStyles(); const { formatMessage } = useSafeIntl(); - const params: Params = useParamsObject(baseUrl); - const { data: dataStats, isLoading: isLoadingDataStats } = useGetFormStats( - baseUrl, - '/api/instances/stats/', - ['instances', 'stats'], - ); + const params: FormStatsParams = useParamsObject(baseUrl); + const { data: dataStats, isLoading: isLoadingDataStats } = + useGetPerFormStats(params); const { data: dataStatsSum, isLoading: isLoadingDataStatsSum } = - useGetFormStats(baseUrl, '/api/instances/stats_sum/', [ - 'instances', - 'stats_sum', - ]); + useGetFormStatsSum(params); return ( <> @@ -47,11 +43,11 @@ export const FormsStats: FunctionComponent = () => { /> - + - + - + diff --git a/hat/assets/js/apps/Iaso/domains/forms/types/formStats.ts b/hat/assets/js/apps/Iaso/domains/forms/types/formStats.ts new file mode 100644 index 0000000000..fdc07c2d7d --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/forms/types/formStats.ts @@ -0,0 +1,7 @@ +export type ApiFormStatsParams = { + params: FormStatsParams; + url: string; + queryKey: string[]; +}; + +export type FormStatsParams = { accountId: string; projectIds?: string };