diff --git a/hat/assets/js/apps/Iaso/components/DisplayIfUserHasPerm.tsx b/hat/assets/js/apps/Iaso/components/DisplayIfUserHasPerm.tsx index c504039cd6..191a9f7054 100644 --- a/hat/assets/js/apps/Iaso/components/DisplayIfUserHasPerm.tsx +++ b/hat/assets/js/apps/Iaso/components/DisplayIfUserHasPerm.tsx @@ -1,5 +1,8 @@ import { FunctionComponent, ReactElement } from 'react'; -import { userHasOneOfPermissions } from '../domains/users/utils'; +import { + userHasAllPermissions, + userHasOneOfPermissions, +} from '../domains/users/utils'; import { useCurrentUser } from '../utils/usersUtils'; type Props = { @@ -10,10 +13,16 @@ type Props = { export const DisplayIfUserHasPerm: FunctionComponent = ({ permissions, children, + strict = false, }) => { const currentUser = useCurrentUser(); - if (userHasOneOfPermissions(permissions, currentUser) && children) { + if (strict) { + if (userHasAllPermissions(permissions, currentUser) && children) { + return children; + } + } else if (userHasOneOfPermissions(permissions, currentUser) && children) { return children; } + return null; }; diff --git a/hat/assets/js/apps/Iaso/constants/routes.tsx b/hat/assets/js/apps/Iaso/constants/routes.tsx index 6a8ee4d1c8..684978384d 100644 --- a/hat/assets/js/apps/Iaso/constants/routes.tsx +++ b/hat/assets/js/apps/Iaso/constants/routes.tsx @@ -1,53 +1,53 @@ /* eslint-disable react/jsx-props-no-spreading */ import React, { ReactElement } from 'react'; +import PageError from '../components/errors/PageError'; +import { Assignments } from '../domains/assignments'; +import Completeness from '../domains/completeness'; +import { CompletenessStats } from '../domains/completenessStats'; +import DataSources from '../domains/dataSources'; +import { Details as DataSourceDetail } from '../domains/dataSources/details'; +import Devices from '../domains/devices'; +import { Beneficiaries } from '../domains/entities'; +import { VisitDetails } from '../domains/entities/components/VisitDetails'; +import { Details as BeneficiaryDetail } from '../domains/entities/details'; +import { DuplicateDetails } from '../domains/entities/duplicates/details/DuplicateDetails'; +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 { OrgUnits } from '../domains/orgUnits'; -import { Links } from '../domains/links'; -import Runs from '../domains/links/Runs'; -import OrgUnitDetail from '../domains/orgUnits/details'; -import Completeness from '../domains/completeness'; import Instances from '../domains/instances'; import CompareSubmissions from '../domains/instances/compare'; +import { CompareInstanceLogs } from '../domains/instances/compare/components/CompareInstanceLogs'; import InstanceDetail from '../domains/instances/details'; +import { Links } from '../domains/links'; +import Runs from '../domains/links/Runs'; import Mappings from '../domains/mappings'; import MappingDetails from '../domains/mappings/details'; -import { Users } from '../domains/users'; -import { UserRoles } from '../domains/userRoles'; import { Modules } from '../domains/modules'; -import { Projects } from '../domains/projects'; -import DataSources from '../domains/dataSources'; -import { Details as DataSourceDetail } from '../domains/dataSources/details'; -import Tasks from '../domains/tasks'; -import Devices from '../domains/devices'; -import { CompletenessStats } from '../domains/completenessStats'; +import { OrgUnits } from '../domains/orgUnits'; +import OrgUnitDetail from '../domains/orgUnits/details'; import Groups from '../domains/orgUnits/groups'; import Types from '../domains/orgUnits/orgUnitTypes'; -import { Beneficiaries } from '../domains/entities'; -import { Details as BeneficiaryDetail } from '../domains/entities/details'; -import { EntityTypes } from '../domains/entities/entityTypes'; -import PageError from '../components/errors/PageError'; -import { baseUrls } from './urls'; +import { ReviewOrgUnitChanges } from '../domains/orgUnits/reviewChanges/ReviewOrgUnitChanges'; import Pages from '../domains/pages'; +import { LotsPayments } from '../domains/payments/LotsPayments'; +import { PotentialPayments } from '../domains/payments/PotentialPayments'; import { Planning } from '../domains/plannings'; -import { Teams } from '../domains/teams'; +import { Projects } from '../domains/projects'; +import { Registry } from '../domains/registry'; +import { SetupAccount } from '../domains/setup'; import { Storages } from '../domains/storages'; +import { Details as StorageDetails } from '../domains/storages/details'; +import Tasks from '../domains/tasks'; +import { Teams } from '../domains/teams'; +import { UserRoles } from '../domains/userRoles'; +import { Users } from '../domains/users'; import { Workflows } from '../domains/workflows'; import { Details as WorkflowDetails } from '../domains/workflows/details'; -import { Details as StorageDetails } from '../domains/storages/details'; -import { Assignments } from '../domains/assignments'; -import { CompareInstanceLogs } from '../domains/instances/compare/components/CompareInstanceLogs'; -import { Registry } from '../domains/registry'; import { SHOW_PAGES } from '../utils/featureFlags'; -import { Duplicates } from '../domains/entities/duplicates/list/Duplicates'; -import { DuplicateDetails } from '../domains/entities/duplicates/details/DuplicateDetails'; -import { ReviewOrgUnitChanges } from '../domains/orgUnits/reviewChanges/ReviewOrgUnitChanges'; -import { VisitDetails } from '../domains/entities/components/VisitDetails'; import * as Permission from '../utils/permissions'; -import { SetupAccount } from '../domains/setup'; -import { PotentialPayments } from '../domains/payments/PotentialPayments'; -import { LotsPayments } from '../domains/payments/LotsPayments'; +import { baseUrls } from './urls'; export type RoutePath = { baseUrl: string; @@ -165,7 +165,7 @@ export const orgUnitChangeRequestPath = { export const registryPath = { baseUrl: baseUrls.registry, routerUrl: `${baseUrls.registry}/*`, - permissions: [Permission.REGISTRY], + permissions: [Permission.REGISTRY_READ, Permission.REGISTRY_WRITE], element: , }; diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/en.json b/hat/assets/js/apps/Iaso/domains/app/translations/en.json index a8f0d1430a..d9008f3b85 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/en.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/en.json @@ -939,7 +939,8 @@ "iaso.permissions.iaso_polio_vaccine_supply_chain_read_tooltip": "See summary of vaccine supply chain, by country and vaccine", "iaso.permissions.iaso_polio_vaccine_supply_chain_write": "Polio vaccine supply chain - Write", "iaso.permissions.iaso_polio_vaccine_supply_chain_write_tooltip": "Edit and add supply chain data", - "iaso.permissions.iaso_registry": "Registry", + "iaso.permissions.iaso_registry_read": "Registry - Read", + "iaso.permissions.iaso_registry_write": "Registry - Write", "iaso.permissions.iaso_workflows": "Workflows", "iaso.permissions.iaso_write_sources": "Geo data sources - Read and Write", "iaso.permissions.links": "Geo data sources matching", @@ -972,7 +973,8 @@ "iaso.permissions.tooltip.iaso_org_unit_types": "Manage types of organisation units, i.e. define the different levels of the pyramid (eg Country/Region/District/Facility/etc)", "iaso.permissions.tooltip.iaso_page_write": "External links management: create or edit an external link", "iaso.permissions.tooltip.iaso_polio_notifications": "Manage polio notifications - Read and Write", - "iaso.permissions.tooltip.iaso_registry": "Summary view of data collected per organisation unit", + "iaso.permissions.tooltip.iaso_registry_read": "Summary view of data collected per organisation unit - Read", + "iaso.permissions.tooltip.iaso_registry_write": "Summary view of data collected per organisation unit - Write", "iaso.permissions.tooltip.iaso_write_sources": "Manage multiple geo data sources: create or edit sources (name, description, project(s), default version, DHIS2 links)", "iaso.permissions.tooltip.links": "Match multiple geo data sources according to specific criteria and algorithms", "iaso.permissions.tooltip.mappings": "Match DHIS2 and IASO data elements for data exchanges", diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json index 920f80ec29..49718e078f 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json @@ -939,7 +939,8 @@ "iaso.permissions.iaso_polio_vaccine_supply_chain_read_tooltip": "Voir le résumé des données de chaîne d'approvisionnement", "iaso.permissions.iaso_polio_vaccine_supply_chain_write": "Polio: chaîne d'approvisionnement - Ecriture", "iaso.permissions.iaso_polio_vaccine_supply_chain_write_tooltip": "Editer et ajouter des données", - "iaso.permissions.iaso_registry": "Registre", + "iaso.permissions.iaso_registry_read": "Registre - Lecture", + "iaso.permissions.iaso_registry_write": "Registre - Ecriture", "iaso.permissions.iaso_workflows": "Workflows", "iaso.permissions.iaso_write_sources": "Sources de données géo - Lecture et écriture", "iaso.permissions.links": "Liens entre sources de données géo", @@ -972,7 +973,8 @@ "iaso.permissions.tooltip.iaso_org_unit_types": "Gestion des types d’unité d’organisation, i.e. définir les différents niveaux de la pyramide (ex: Pays/Région/Aire/Formation sanitaire)", "iaso.permissions.tooltip.iaso_page_write": "Gestion des liens externes : créer ou modifier un lien externe", "iaso.permissions.tooltip.iaso_polio_notifications": "Manage polio notifications - Read and Write", - "iaso.permissions.tooltip.iaso_registry": "Vue résumée des données collectées par unité d’organisation", + "iaso.permissions.tooltip.iaso_registry_read": "Vue résumée des données collectées par unité d’organisation - Lecture", + "iaso.permissions.tooltip.iaso_registry_write": "Vue résumée des données collectées par unité d’organisation - Ecriture", "iaso.permissions.tooltip.iaso_write_sources": "Gestion des sources de données géographiques: créer ou éditer (nom, description, projet(s), version par défaut, liens DHIS2)", "iaso.permissions.tooltip.links": "Liens entre les sources de données géographiques selon des critères spécifiques et des algorithmes", "iaso.permissions.tooltip.mappings": "Edition des liens entre les éléments de données DHIS2 et IASO pour les échanges de données", diff --git a/hat/assets/js/apps/Iaso/domains/instances/utils/index.tsx b/hat/assets/js/apps/Iaso/domains/instances/utils/index.tsx index db4680ef0a..a189956039 100644 --- a/hat/assets/js/apps/Iaso/domains/instances/utils/index.tsx +++ b/hat/assets/js/apps/Iaso/domains/instances/utils/index.tsx @@ -1,53 +1,53 @@ /* eslint-disable camelcase */ -import React, { - FunctionComponent, - ReactElement, - useMemo, - useCallback, -} from 'react'; -import moment from 'moment'; -import { Tooltip } from '@mui/material'; -import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; import CallMade from '@mui/icons-material/CallMade'; +import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; +import { Tooltip } from '@mui/material'; import { - truncateText, - getTableUrl, Column, - Setting, + LinkWithLocation, RenderCell, + Setting, + getTableUrl, + truncateText, useSafeIntl, - LinkWithLocation, } from 'bluesquare-components'; +import moment from 'moment'; +import React, { + FunctionComponent, + ReactElement, + useCallback, + useMemo, +} from 'react'; import instancesTableColumns from '../config'; import MESSAGES from '../messages'; -import { VisibleColumn } from '../types/visibleColumns'; import { Instance, ShortFile } from '../types/instance'; +import { VisibleColumn } from '../types/visibleColumns'; +import { getCookie } from '../../../utils/cookies'; import { - apiDateTimeFormat, apiDateFormat, + apiDateTimeFormat, getFromDateString, getToDateString, } from '../../../utils/dates'; -import ActionTableColumnComponent from '../components/ActionTableColumnComponent'; import { Form, PossibleField } from '../../forms/types/forms'; -import { getCookie } from '../../../utils/cookies'; +import ActionTableColumnComponent from '../components/ActionTableColumnComponent'; import DeleteDialog from '../components/DeleteInstanceDialog'; import ExportInstancesDialogComponent from '../components/ExportInstancesDialogComponent'; -import { fetchLatestOrgUnitLevelId } from '../../orgUnits/utils'; import { baseUrls } from '../../../constants/urls'; +import { fetchLatestOrgUnitLevelId } from '../../orgUnits/utils'; import { Selection } from '../../orgUnits/types/selection'; import { userHasOneOfPermissions, userHasPermission } from '../../users/utils'; -import { useCurrentUser } from '../../../utils/usersUtils'; import * as Permission from '../../../utils/permissions'; -import { INSTANCE_METAS_FIELDS } from '../constants'; +import { useCurrentUser } from '../../../utils/usersUtils'; import { InstanceMetasField } from '../components/ColumnSelect'; +import { INSTANCE_METAS_FIELDS } from '../constants'; const NO_VALUE = '/'; // eslint-disable-next-line no-unused-vars @@ -287,6 +287,7 @@ export const useInstancesColumns = ( }); tableColumns = tableColumns.concat(childrenArray); if ( + userHasPermission(Permission.REGISTRY_WRITE, currentUser) && userHasOneOfPermissions( [Permission.SUBMISSIONS_UPDATE, Permission.SUBMISSIONS], currentUser, diff --git a/hat/assets/js/apps/Iaso/domains/registry/components/ActionCell.tsx b/hat/assets/js/apps/Iaso/domains/registry/components/ActionCell.tsx index 84fd1beaef..7173b6a309 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/components/ActionCell.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/components/ActionCell.tsx @@ -1,27 +1,24 @@ +import { IconButton as IconButtonComponent } from 'bluesquare-components'; import React, { FunctionComponent } from 'react'; import { useQueryClient } from 'react-query'; -import { IconButton as IconButtonComponent } from 'bluesquare-components'; import MESSAGES from '../messages'; -import { userHasPermission } from '../../users/utils'; -import { useCurrentUser } from '../../../utils/usersUtils'; -import EnketoIcon from '../../instances/components/EnketoIcon'; import DeleteDialog from '../../../components/dialogs/DeleteDialogComponent'; +import EnketoIcon from '../../instances/components/EnketoIcon'; import { LinkToInstance } from '../../instances/components/LinkToInstance'; -import { useGetEnketoUrl } from '../hooks/useGetEnketoUrl'; +import { DisplayIfUserHasPerm } from '../../../components/DisplayIfUserHasPerm'; +import * as Permissions from '../../../utils/permissions'; import { useDeleteInstance } from '../../instances/hooks/requests/useDeleteInstance'; -import * as Permission from '../../../utils/permissions'; +import { useGetEnketoUrl } from '../hooks/useGetEnketoUrl'; type Props = { settings: any; }; export const ActionCell: FunctionComponent = ({ settings }) => { - const user = useCurrentUser(); - const queryClient = useQueryClient(); const getEnketoUrl = useGetEnketoUrl( window.location.href, @@ -37,7 +34,9 @@ export const ActionCell: FunctionComponent = ({ settings }) => { return (
- {userHasPermission(Permission.SUBMISSIONS_UPDATE, user) && ( + <> getEnketoUrl()} @@ -54,7 +53,7 @@ export const ActionCell: FunctionComponent = ({ settings }) => { } /> - )} +
); }; diff --git a/hat/assets/js/apps/Iaso/domains/registry/components/Instances.tsx b/hat/assets/js/apps/Iaso/domains/registry/components/Instances.tsx index 7832b6c5c3..034b3abbfe 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/components/Instances.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/components/Instances.tsx @@ -1,31 +1,29 @@ import { Box, Grid, Tab, Tabs } from '@mui/material'; +import { Column, useRedirectToReplace } from 'bluesquare-components'; import React, { FunctionComponent, useCallback, useMemo, useState, } from 'react'; -import { Column, useRedirectToReplace } from 'bluesquare-components'; +import { DisplayIfUserHasPerm } from '../../../components/DisplayIfUserHasPerm'; import DownloadButtonsComponent from '../../../components/DownloadButtonsComponent'; import InputComponent from '../../../components/forms/InputComponent'; import { TableWithDeepLink } from '../../../components/tables/TableWithDeepLink'; +import { baseUrls } from '../../../constants/urls'; +import * as Permissions from '../../../utils/permissions'; +import { Form } from '../../forms/types/forms'; import { ColumnSelect } from '../../instances/components/ColumnSelect'; -import { ActionCell } from './ActionCell'; -import { MissingInstanceDialog } from './MissingInstanceDialog'; import { OrgunitType } from '../../orgUnits/types/orgunitTypes'; -import { RegistryParams } from '../types'; -import { OrgunitTypeRegistry } from '../types/orgunitTypes'; -import { RegistryDetailParams } from '../types'; -import { Form } from '../../forms/types/forms'; +import { INSTANCE_METAS_FIELDS, defaultSorted } from '../config'; +import { useGetEmptyInstanceOrgUnits } from '../hooks/useGetEmptyInstanceOrgUnits'; import { useGetForms } from '../hooks/useGetForms'; import { useGetInstanceApi, useGetInstances } from '../hooks/useGetInstances'; -import { baseUrls } from '../../../constants/urls'; -import * as Permission from '../../../utils/permissions'; -import { useCurrentUser } from '../../../utils/usersUtils'; -import { userHasPermission } from '../../users/utils'; -import { defaultSorted, INSTANCE_METAS_FIELDS } from '../config'; -import { useGetEmptyInstanceOrgUnits } from '../hooks/useGetEmptyInstanceOrgUnits'; import MESSAGES from '../messages'; +import { RegistryParams } from '../types'; +import { OrgunitTypeRegistry } from '../types/orgunitTypes'; +import { ActionCell } from './ActionCell'; +import { MissingInstanceDialog } from './MissingInstanceDialog'; type Props = { isLoading: boolean; @@ -38,7 +36,6 @@ export const Instances: FunctionComponent = ({ subOrgUnitTypes, params, }) => { - const currentUser = useCurrentUser(); const redirectToReplace = useRedirectToReplace(); const [tableColumns, setTableColumns] = useState([]); const { formIds, tab } = params; @@ -154,20 +151,25 @@ export const Instances: FunctionComponent = ({ )} /> )} - - - + + + + = ({ mt={2} > {orgUnitsWithoutCurrentForm && - orgUnitsWithoutCurrentForm.count > 0 && - userHasPermission( - Permission.SUBMISSIONS_UPDATE, - currentUser, - ) && ( - + orgUnitsWithoutCurrentForm.count > + 0 && ( + + + )} @@ -212,7 +219,9 @@ export const Instances: FunctionComponent = ({ columns={tableColumns} count={data?.count ?? 0} params={params} - extraProps={{ loading: isFetchingList }} + extraProps={{ + loading: isFetchingList, + }} /> diff --git a/hat/assets/js/apps/Iaso/domains/registry/components/LinkToRegistry.tsx b/hat/assets/js/apps/Iaso/domains/registry/components/LinkToRegistry.tsx index 1feb2a9f63..3888f6e106 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/components/LinkToRegistry.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/components/LinkToRegistry.tsx @@ -3,11 +3,11 @@ import { LinkTo } from '../../../components/nav/LinkTo'; import { baseUrls } from '../../../constants/urls'; import { useCurrentUser } from '../../../utils/usersUtils'; import { OrgUnit, ShortOrgUnit } from '../../orgUnits/types/orgUnit'; -import { userHasPermission } from '../../users/utils'; +import { userHasOneOfPermissions } from '../../users/utils'; import MESSAGES from '../messages'; -import { REGISTRY } from '../../../utils/permissions'; +import { REGISTRY_READ, REGISTRY_WRITE } from '../../../utils/permissions'; type Props = { orgUnit?: OrgUnit | ShortOrgUnit; @@ -29,7 +29,9 @@ export const LinkToRegistry: FunctionComponent = ({ color = 'inherit', }) => { const user = useCurrentUser(); - const condition = userHasPermission(REGISTRY, user) && Boolean(orgUnit); + const condition = + userHasOneOfPermissions([REGISTRY_READ, REGISTRY_WRITE], user) && + Boolean(orgUnit); const url = `/${baseUrls.registry}/orgUnitId/${orgUnit?.id}`; const text = orgUnit?.name; diff --git a/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/InstanceTitle.tsx b/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/InstanceTitle.tsx index 49f221aa31..ed5fd84464 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/InstanceTitle.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/InstanceTitle.tsx @@ -18,7 +18,7 @@ import EnketoIcon from '../../../instances/components/EnketoIcon'; import { DisplayIfUserHasPerm } from '../../../../components/DisplayIfUserHasPerm'; import InputComponent from '../../../../components/forms/InputComponent'; import { baseUrls } from '../../../../constants/urls'; -import * as Permission from '../../../../utils/permissions'; +import * as Permissions from '../../../../utils/permissions'; import { LinkToInstance } from '../../../instances/components/LinkToInstance'; import { Instance } from '../../../instances/types/instance'; import { OrgUnit } from '../../../orgUnits/types/orgUnit'; @@ -110,36 +110,40 @@ export const InstanceTitle: FunctionComponent = ({ /> {currentInstance && ( - - - - getEnketoUrl()} - overrideIcon={EnketoIcon} + + + + getEnketoUrl()} + overrideIcon={EnketoIcon} + color="secondary" + iconSize="small" + size="small" + tooltipMessage={MESSAGES.editOnEnketo} + /> + + - - - - + + + )} ); diff --git a/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/OrgUnitTitle.tsx b/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/OrgUnitTitle.tsx index 9c07d2210b..3753201c09 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/OrgUnitTitle.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/components/selectedOrgUnit/OrgUnitTitle.tsx @@ -7,6 +7,8 @@ import React, { FunctionComponent } from 'react'; import { baseUrls } from '../../../../constants/urls'; import MESSAGES from '../../messages'; +import { DisplayIfUserHasPerm } from '../../../../components/DisplayIfUserHasPerm'; +import * as Permissions from '../../../../utils/permissions'; import { OrgUnit } from '../../../orgUnits/types/orgUnit'; import { RegistryParams } from '../../types'; import { LinkToRegistry } from '../LinkToRegistry'; @@ -55,24 +57,30 @@ export const OrgUnitTitle: FunctionComponent = ({ orgUnit, params }) => { className={classes.paperTitleButtonContainer} > - {isRootOrgUnit && ( - - )} - + + <> + {isRootOrgUnit && ( + + )} + + + {!isRootOrgUnit && ( = ({ isFetching: isFetchingOrgUnit, }) => { const classes: Record = useStyles(); + const currentUser = useCurrentUser(); const currentInstanceId = useMemo(() => { return params.submissionId || orgUnit?.reference_instances?.[0]?.id; }, [params.submissionId, orgUnit]); const { data: currentInstance, isFetching: isFetchingCurrentInstance } = useGetInstance(currentInstanceId, false); - const { data: instances, isFetching } = useGetOrgUnitInstances(orgUnit?.id); + const { data: instances, isFetching } = useGetOrgUnitInstances( + orgUnit?.id, + !userHasPermission(Permissions.REGISTRY_WRITE, currentUser), + ); if (!orgUnit) { return null; diff --git a/hat/assets/js/apps/Iaso/domains/registry/config.tsx b/hat/assets/js/apps/Iaso/domains/registry/config.tsx index 9375d6ed31..0cbc1fcd10 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/config.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/config.tsx @@ -44,7 +44,7 @@ export const INSTANCE_METAS_FIELDS: InstanceMetasField[] = [ }, { key: 'created_at', - active: false, + active: true, tableOrder: 5, type: 'info', }, diff --git a/hat/assets/js/apps/Iaso/domains/registry/hooks/useGetInstances.tsx b/hat/assets/js/apps/Iaso/domains/registry/hooks/useGetInstances.tsx index f4cf4e63b1..9966338d86 100644 --- a/hat/assets/js/apps/Iaso/domains/registry/hooks/useGetInstances.tsx +++ b/hat/assets/js/apps/Iaso/domains/registry/hooks/useGetInstances.tsx @@ -78,10 +78,12 @@ export const useGetInstances = ( export const useGetOrgUnitInstances = ( orgUnitId?: number, + onlyReference = false, ): UseQueryResult => { const apiParams: Record = { orgUnitId, showDeleted: false, + onlyReference, }; const url = makeUrlWithParams('/api/instances/', apiParams); return useSnackQuery({ diff --git a/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts b/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts index 4d2a3aebef..45db40466d 100644 --- a/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts +++ b/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts @@ -245,13 +245,23 @@ const PERMISSIONS_MESSAGES = defineMessages({ defaultMessage: 'View and edit the entity duplicates - e.g. decide to merge or not similar entities', }, - iaso_registry: { - id: 'iaso.permissions.iaso_registry', - defaultMessage: 'Registry', + iaso_registry_write: { + id: 'iaso.permissions.iaso_registry_write', + defaultMessage: 'Registry - Write', }, - iaso_registry_tooltip: { - id: 'iaso.permissions.tooltip.iaso_registry', - defaultMessage: 'Summary view of data collected per organisation unit', + iaso_registry_read: { + id: 'iaso.permissions.iaso_registry_read', + defaultMessage: 'Registry - Read', + }, + iaso_registry_read_tooltip: { + id: 'iaso.permissions.tooltip.iaso_registry_read', + defaultMessage: + 'Summary view of data collected per organisation unit -- Read', + }, + iaso_registry_write_tooltip: { + id: 'iaso.permissions.tooltip.iaso_registry_write', + defaultMessage: + 'Summary view of data collected per organisation unit -- Write', }, iaso_org_unit_types: { id: 'iaso.permissions.iaso_org_unit_types', diff --git a/hat/assets/js/apps/Iaso/domains/users/utils.js b/hat/assets/js/apps/Iaso/domains/users/utils.js index cefdf6e792..388c75639c 100644 --- a/hat/assets/js/apps/Iaso/domains/users/utils.js +++ b/hat/assets/js/apps/Iaso/domains/users/utils.js @@ -36,6 +36,19 @@ export const userHasOneOfPermissions = (permissions = [], user) => { }); return isAuthorised; }; +/** + * Check if user has all the specified permissions. + * + * @param {Array} permissions - Array of permissions to check. + * @param {Object} user - User object to check permissions against. + * @return {Boolean} - Returns true if user has all the permissions, otherwise false. + */ +export const userHasAllPermissions = (permissions, user) => { + if (!user || !Array.isArray(permissions) || permissions.length === 0) { + return false; + } + return permissions.every(permission => userHasPermission(permission, user)); +}; /** * list all submenu permission diff --git a/hat/assets/js/apps/Iaso/utils/permissions.ts b/hat/assets/js/apps/Iaso/utils/permissions.ts index b2e153c1f0..3bcbd38a07 100644 --- a/hat/assets/js/apps/Iaso/utils/permissions.ts +++ b/hat/assets/js/apps/Iaso/utils/permissions.ts @@ -22,7 +22,8 @@ const POLIO_SUPPLY_CHAIN_WRITE = 'iaso_polio_vaccine_supply_chain_write'; const POLIO_VACCINE_STOCK_READ = 'iaso_polio_vaccine_stock_management_read'; const POLIO_VACCINE_STOCK_WRITE = 'iaso_polio_vaccine_stock_management_write'; const PROJECTS = 'iaso_projects'; -const REGISTRY = 'iaso_registry'; +const REGISTRY_WRITE = 'iaso_registry_write'; +const REGISTRY_READ = 'iaso_registry_read'; const ORG_UNITS_CHANGE_REQUEST_REVIEW = 'iaso_org_unit_change_request_review'; const SOURCES = 'iaso_sources'; const SOURCE_WRITE = 'iaso_write_sources'; @@ -44,38 +45,39 @@ export { DATA_DEVICES, DATA_TASKS, ENTITIES, - ENTITY_TYPE_WRITE, ENTITIES_DUPLICATE_READ, ENTITIES_DUPLICATE_WRITE, + ENTITY_TYPE_WRITE, FORMS, LINKS, MAPPINGS, + MODULES, + ORG_UNITS, ORG_UNITS_CHANGE_REQUEST_REVIEW, ORG_UNIT_GROUPS, ORG_UNIT_TYPES, - ORG_UNITS, PAGES, PAGE_WRITE, + PAYMENTS, PLANNINGS, POLIO, POLIO_CONFIG, + POLIO_NOTIFICATIONS, POLIO_SUPPLY_CHAIN_READ, POLIO_SUPPLY_CHAIN_WRITE, POLIO_VACCINE_STOCK_READ, POLIO_VACCINE_STOCK_WRITE, - POLIO_NOTIFICATIONS, PROJECTS, - REGISTRY, + REGISTRY_READ, + REGISTRY_WRITE, SOURCES, SOURCE_WRITE, STORAGES, SUBMISSIONS, SUBMISSIONS_UPDATE, TEAMS, - USER_ROLES, USERS_ADMIN, USERS_MANAGEMENT, + USER_ROLES, WORKFLOWS, - MODULES, - PAYMENTS, }; diff --git a/hat/menupermissions/constants.py b/hat/menupermissions/constants.py index a27b520445..b09bf597a2 100644 --- a/hat/menupermissions/constants.py +++ b/hat/menupermissions/constants.py @@ -45,7 +45,7 @@ "iaso_polio_vaccine_authorizations_read_only", "iaso_polio_vaccine_authorizations_admin", ], - "REGISTRY": ["iaso_registry", "iaso_org_unit_change_request_review"], + "REGISTRY": ["iaso_registry_write", "iaso_registry_read", "iaso_org_unit_change_request_review"], "PAYMENTS": ["iaso_payments"], } diff --git a/hat/menupermissions/migrations/0060_alter_custompermissionsupport_options.py b/hat/menupermissions/migrations/0060_alter_custompermissionsupport_options.py new file mode 100644 index 0000000000..ed505c4929 --- /dev/null +++ b/hat/menupermissions/migrations/0060_alter_custompermissionsupport_options.py @@ -0,0 +1,101 @@ +# Generated by Django 4.2.11 on 2024-05-21 12:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("menupermissions", "0059_alter_custompermissionsupport_options"), + ] + + operations = [ + migrations.AlterModelOptions( + name="custompermissionsupport", + options={ + "managed": False, + "permissions": ( + ("iaso_forms", "Formulaires"), + ("iaso_mappings", "Correspondances avec DHIS2"), + ("iaso_modules", "modules"), + ("iaso_completeness", "Complétude des données"), + ("iaso_org_units", "Unités d'organisations"), + ("iaso_registry_write", "Editer le Registre"), + ("iaso_registry_read", "Lire le Registre"), + ("iaso_links", "Correspondances sources"), + ("iaso_users", "Users"), + ("iaso_users_managed", "Users managed"), + ("iaso_pages", "Pages"), + ("iaso_projects", "Projets"), + ("iaso_sources", "Sources"), + ("iaso_data_tasks", "Tâches"), + ("iaso_polio", "Polio"), + ("iaso_polio_config", "Polio config"), + ("iaso_polio_notifications", "Polio notifications"), + ("iaso_submissions", "Soumissions"), + ("iaso_update_submission", "Editer soumissions"), + ("iaso_planning", "Planning"), + ("iaso_reports", "Reports"), + ("iaso_teams", "Equipes"), + ("iaso_assignments", "Attributions"), + ("iaso_polio_budget", "Budget Polio"), + ("iaso_entities", "Entities"), + ("iaso_entity_type_write", "Write entity type"), + ("iaso_storages", "Storages"), + ("iaso_completeness_stats", "Completeness stats"), + ("iaso_workflows", "Workflows"), + ("iaso_polio_budget_admin", "Budget Polio Admin"), + ("iaso_entity_duplicates_read", "Read Entity duplicates"), + ("iaso_entity_duplicates_write", "Write Entity duplicates"), + ("iaso_user_roles", "Manage user roles"), + ("iaso_datastore_read", "Read data store"), + ("iaso_datastore_write", "Write data store"), + ("iaso_org_unit_types", "Org unit types"), + ("iaso_org_unit_groups", "Org unit groups"), + ("iaso_org_unit_change_request_review", "Org unit change request review"), + ("iaso_write_sources", "Write data source"), + ("iaso_page_write", "Write page"), + ("iaso_payments", "Payments page"), + ("iaso_polio_vaccine_authorizations_read_only", "Polio Vaccine Authorizations Read Only"), + ("iaso_polio_vaccine_authorizations_admin", "Polio Vaccine Authorizations Admin"), + ("iaso_polio_vaccine_supply_chain_read", "Polio Vaccine Supply Chain Read"), + ("iaso_polio_vaccine_supply_chain_write", "Polio Vaccine Supply Chain Write"), + ("iaso_polio_vaccine_stock_management_read", "Polio Vaccine Stock Management Read"), + ("iaso_polio_vaccine_stock_management_write", "Polio Vaccine Stock Management Write"), + ("iaso_trypelim_anonymous", "Anonymisation des patients"), + ("iaso_trypelim_management_areas", "Areas"), + ("iaso_trypelim_management_edit_areas", "Edit areas"), + ("iaso_trypelim_management_edit_shape_areas", "Edit areas shapes"), + ("iaso_trypelim_case_cases", "Cases"), + ("iaso_trypelim_case_analysis", "Cases analysis"), + ("iaso_trypelim_management_coordinations", "Coordinations"), + ("iaso_trypelim_management_devices", "Devices"), + ("iaso_trypelim_datas_download", "Téléchargement de données"), + ("iaso_trypelim_duplicates", "Doublons"), + ("iaso_trypelim_datas_patient_edition", "Edition d'un patient"), + ("iaso_trypelim_stats_graphs", "Graphs"), + ("iaso_trypelim_management_health_structures", "Health facilities"), + ("iaso_trypelim_lab", "Labo"), + ("iaso_trypelim_labupload", "Labo import"), + ("iaso_trypelim_locator", "Locator"), + ("iaso_trypelim_plannings_macroplanning", "Macroplanning"), + ("iaso_trypelim_plannings_microplanning", "Microplanning"), + ("iaso_trypelim_modifications", "Modifications"), + ("iaso_trypelim_management_plannings", "Plannings"), + ("iaso_trypelim_management_plannings_template", "Plannings template"), + ("iaso_trypelim_qualitycontrol", "Quality control"), + ("iaso_trypelim_case_reconciliation", "Reconciliation"), + ("iaso_trypelim_plannings_routes", "Routes"), + ("iaso_trypelim_datasets_datauploads", "Upload of cases files"), + ("iaso_trypelim_datasets_villageuploads", "Upload of villages files"), + ("iaso_trypelim_management_users", "Users"), + ("iaso_trypelim_vectorcontrol", "Vector control"), + ("iaso_trypelim_vectorcontrolupload", "Vector control import Gpx"), + ("iaso_trypelim_management_villages", "Villages"), + ("iaso_trypelim_management_workzones", "Work zones"), + ("iaso_trypelim_management_zones", "Zones"), + ("iaso_trypelim_management_edit_zones", "Edit zones"), + ("iaso_trypelim_management_edit_shape_zones", "Edit zones shapes"), + ), + }, + ), + ] diff --git a/hat/menupermissions/models.py b/hat/menupermissions/models.py index b6044d5f79..99ebe8ead7 100644 --- a/hat/menupermissions/models.py +++ b/hat/menupermissions/models.py @@ -16,6 +16,7 @@ The frontend is getting the list of existing permission from the `/api/permissions/` endpoint """ + from django.conf import LazySettings from django.contrib.contenttypes.models import ContentType from django.db import models @@ -50,7 +51,8 @@ _POLIO_CONFIG = "iaso_polio_config" _POLIO_NOTIFICATIONS = "iaso_polio_notifications" _PROJECTS = "iaso_projects" -_REGISTRY = "iaso_registry" +_REGISTRY_WRITE = "iaso_registry_write" +_REGISTRY_READ = "iaso_registry_read" _REPORTS = "iaso_reports" _SOURCE_WRITE = "iaso_write_sources" _SOURCES = "iaso_sources" @@ -140,7 +142,8 @@ POLIO_VACCINE_SUPPLY_CHAIN_READ = _PREFIX + _POLIO_VACCINE_SUPPLY_CHAIN_READ POLIO_VACCINE_SUPPLY_CHAIN_WRITE = _PREFIX + _POLIO_VACCINE_SUPPLY_CHAIN_WRITE PROJECTS = _PREFIX + _PROJECTS -REGISTRY = _PREFIX + _REGISTRY +REGISTRY_WRITE = _PREFIX + _REGISTRY_WRITE +REGISTRY_READ = _PREFIX + _REGISTRY_READ REPORTS = _PREFIX + _REPORTS SOURCE_WRITE = _PREFIX + _SOURCE_WRITE SOURCES = _PREFIX + _SOURCES @@ -222,7 +225,8 @@ class Meta: (_MODULES, _("modules")), (_COMPLETENESS, _("Complétude des données")), (_ORG_UNITS, _("Unités d'organisations")), - (_REGISTRY, _("Registre")), + (_REGISTRY_WRITE, _("Editer le Registre")), + (_REGISTRY_READ, _("Lire le Registre")), (_LINKS, _("Correspondances sources")), (_USERS_ADMIN, _("Users")), (_USERS_MANAGED, _("Users managed")), diff --git a/iaso/api/completeness_stats.py b/iaso/api/completeness_stats.py index ac93966317..039558c311 100644 --- a/iaso/api/completeness_stats.py +++ b/iaso/api/completeness_stats.py @@ -30,21 +30,19 @@ ``` """ -from typing import Optional, Any -from typing import TypedDict, Mapping, List, Union +from typing import Any, List, Mapping, Optional, TypedDict, Union import rest_framework.fields import rest_framework.renderers import rest_framework_csv.renderers +from django.contrib.auth.models import User from django.core.paginator import Paginator from django.db import models -from django.db.models import QuerySet, OrderBy, Q +from django.db.models import OrderBy, Q, QuerySet from django.db.models.expressions import RawSQL -from django.contrib.auth.models import User from django_cte import With from django_cte.raw import raw_cte_sql -from rest_framework import serializers -from rest_framework import viewsets, permissions +from rest_framework import permissions, serializers, viewsets from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.request import Request @@ -52,13 +50,14 @@ from rest_framework.serializers import ModelSerializer from typing_extensions import Annotated -from iaso.models import OrgUnit, Form, OrgUnitType, Instance, Group -from .common import HasPermission +from hat.menupermissions import models as permission +from iaso.models import Form, Group, Instance, OrgUnit, OrgUnitType +from iaso.utils import geojson_queryset + from ..models.microplanning import Planning, Team from ..models.org_unit import OrgUnitQuerySet from ..periods import Period -from iaso.utils import geojson_queryset -from hat.menupermissions import models as permission +from .common import HasPermission class OrgUnitTypeSerializer(ModelSerializer): @@ -235,7 +234,7 @@ class CompletenessStatsV2ViewSet(viewsets.ViewSet): permission_classes = [ permissions.IsAuthenticated, - HasPermission(permission.COMPLETENESS_STATS, permission.REGISTRY), # type: ignore + HasPermission(permission.COMPLETENESS_STATS, permission.REGISTRY_WRITE, permission.REGISTRY_READ), # type: ignore ] # type: ignore # @swagger_auto_schema(query_serializer=ParamSerializer()) diff --git a/iaso/api/instance_filters.py b/iaso/api/instance_filters.py index 4a66fb3c27..922cc9220c 100644 --- a/iaso/api/instance_filters.py +++ b/iaso/api/instance_filters.py @@ -1,14 +1,14 @@ import datetime import json -from typing import Dict, Any, Optional +from typing import Any, Dict, Optional -from django.shortcuts import get_object_or_404 from django.http import QueryDict +from django.shortcuts import get_object_or_404 from rest_framework.exceptions import ValidationError -from iaso.models import Form -from iaso.periods import Period, DayPeriod from iaso.api import query_params as query +from iaso.models import Form +from iaso.periods import DayPeriod, Period def parse_instance_filters(req: QueryDict) -> Dict[str, Any]: @@ -44,6 +44,7 @@ def parse_instance_filters(req: QueryDict) -> Dict[str, Any]: "device_ownership_id": req.get(query.DEVICE_OWNERSHIP_ID, None), "org_unit_parent_id": req.get(query.ORG_UNIT_PARENT_ID, None), "org_unit_id": req.get(query.ORG_UNIT_ID, None), + "only_reference": req.get(query.ONLY_REFERENCE, None), "period_ids": periods, "periods_bound": periods_bound, "planning_ids": req.get(query.PLANNING_IDS, None), diff --git a/iaso/api/instances.py b/iaso/api/instances.py index 80aecb6c0a..0491edbb63 100644 --- a/iaso/api/instances.py +++ b/iaso/api/instances.py @@ -1,19 +1,17 @@ import json import ntpath from time import gmtime, strftime -from typing import Dict, Any, Union +from typing import Any, Dict, Union import pandas as pd from django.contrib.auth.models import User from django.contrib.gis.geos import Point from django.core.paginator import Paginator -from django.db import connection -from django.db import transaction -from django.db.models import Q, Count, QuerySet -from django.http import StreamingHttpResponse, HttpResponse +from django.db import connection, transaction +from django.db.models import Count, Q, QuerySet +from django.http import HttpResponse, StreamingHttpResponse from django.utils.timezone import now -from rest_framework import serializers, status -from rest_framework import viewsets, permissions +from rest_framework import permissions, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.request import Request @@ -22,26 +20,28 @@ import iaso.periods as periods from hat.api.export_utils import Echo, generate_xlsx, iter_items, timestamp_to_utc_datetime -from hat.audit.models import log_modification, INSTANCE_API +from hat.audit.models import INSTANCE_API, log_modification from hat.common.utils import queryset_iterator +from hat.menupermissions import models as permission from iaso.api.serializers import OrgUnitSerializer from iaso.models import ( + Entity, Instance, - OrgUnit, - Project, InstanceFile, - InstanceQuerySet, InstanceLock, - Entity, + InstanceQuerySet, + OrgUnit, OrgUnitChangeRequest, + Project, ) +from iaso.models.org_unit import OrgUnitReferenceInstance from iaso.utils import timestamp_to_datetime + +from ..models.forms import CR_MODE_IF_REFERENCE_FORM, CR_MODE_NONE from . import common from .comment import UserSerializerForComment -from .common import safe_api_import, TimestampField, FileFormatEnum, CONTENT_TYPE_XLSX, CONTENT_TYPE_CSV -from .instance_filters import parse_instance_filters, get_form_from_instance_filters -from hat.menupermissions import models as permission -from ..models.forms import CR_MODE_NONE, CR_MODE_IF_REFERENCE_FORM +from .common import CONTENT_TYPE_CSV, CONTENT_TYPE_XLSX, FileFormatEnum, TimestampField, safe_api_import +from .instance_filters import get_form_from_instance_filters, parse_instance_filters class InstanceSerializer(serializers.ModelSerializer): @@ -85,7 +85,8 @@ def has_permission(self, request: Request, view): return request.user.is_authenticated and ( request.user.has_perm(permission.FORMS) or request.user.has_perm(permission.SUBMISSIONS) - or request.user.has_perm(permission.REGISTRY) + or request.user.has_perm(permission.REGISTRY_WRITE) + or request.user.has_perm(permission.REGISTRY_READ) ) def has_object_permission(self, request: Request, view, obj: Instance): diff --git a/iaso/api/org_units.py b/iaso/api/org_units.py index 3904a28b3b..2fa1825e76 100644 --- a/iaso/api/org_units.py +++ b/iaso/api/org_units.py @@ -38,7 +38,8 @@ def has_object_permission(self, request, view, obj): request.user.has_perm(permission.FORMS) or request.user.has_perm(permission.ORG_UNITS) or request.user.has_perm(permission.SUBMISSIONS) - or request.user.has_perm(permission.REGISTRY) + or request.user.has_perm(permission.REGISTRY_WRITE) + or request.user.has_perm(permission.REGISTRY_READ) or request.user.has_perm(permission.POLIO) ) ): diff --git a/iaso/api/query_params.py b/iaso/api/query_params.py index 86859e6943..cd14bbceb2 100644 --- a/iaso/api/query_params.py +++ b/iaso/api/query_params.py @@ -31,3 +31,4 @@ STATUS = "status" USER_IDS = "userIds" WITH_LOCATION = "withLocation" +ONLY_REFERENCE = "onlyReference" diff --git a/iaso/models/base.py b/iaso/models/base.py index 56f1f39b45..39f149fc3b 100644 --- a/iaso/models/base.py +++ b/iaso/models/base.py @@ -23,7 +23,7 @@ from django.core.paginator import Paginator from django.core.validators import MinLengthValidator from django.db import models -from django.db.models import Count, FilteredRelation, Q +from django.db.models import Count, Exists, FilteredRelation, OuterRef, Q from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -38,9 +38,9 @@ from .. import periods from ..utils.jsonlogic import jsonlogic_to_q +from ..utils.models.common import get_creator_name from .device import Device, DeviceOwnership from .forms import Form, FormVersion -from ..utils.models.common import get_creator_name logger = getLogger(__name__) @@ -718,6 +718,7 @@ def for_filters( sent_date_to=None, json_content=None, planning_ids=None, + only_reference=None, ): queryset = self if from_date: @@ -741,8 +742,20 @@ def for_filters( if org_unit_type_id: queryset = queryset.filter(org_unit__org_unit_type__in=org_unit_type_id.split(",")) - if org_unit_id: - queryset = queryset.filter(org_unit_id=org_unit_id) + + if only_reference == "true": + if org_unit_id: + # Create a subquery for OrgUnitReferenceInstance that checks for matching org_unit_id and instance_id + subquery = OrgUnitReferenceInstance.objects.filter(org_unit_id=org_unit_id, instance_id=OuterRef("pk")) + queryset = queryset.annotate(has_reference=Exists(subquery)).filter(has_reference=True) + else: + # If no specific org_unit_id is provided, check for any OrgUnitReferenceInstance matching the instance_id + subquery = OrgUnitReferenceInstance.objects.filter(instance_id=OuterRef("pk")) + queryset = queryset.annotate(has_reference=Exists(subquery)).filter(has_reference=True) + else: + if org_unit_id: + # Filter by org unit id if only_reference is not true + queryset = queryset.filter(org_unit_id=org_unit_id) if org_unit_parent_id: # Local import to avoid loop @@ -750,7 +763,6 @@ def for_filters( parent = OrgUnit.objects.get(id=org_unit_parent_id) queryset = queryset.filter(org_unit__path__descendants=parent.path) - if with_location == "true": queryset = queryset.filter(location__isnull=False)