diff --git a/docs/pages/dev/how_to/add_new_permission/add_new_permission.md b/docs/pages/dev/how_to/add_new_permission/add_new_permission.md index 12b63a8856..19b156e824 100644 --- a/docs/pages/dev/how_to/add_new_permission/add_new_permission.md +++ b/docs/pages/dev/how_to/add_new_permission/add_new_permission.md @@ -21,21 +21,29 @@ - If no existing group fits, create one (see exiting groups for inspiration) - If the corresponding group exists add the new permission to that group (see exiting groups for inspiration) -## 4. Make and run migration +## 4. If the added permission must be coupled with another permission like read and edit +- Go to `/hat/menupermissions/constants.py` +- Add the added permission and it's related permission as an item of the `READ_EDIT_PERMISSIONS` dictionnary +- The item should have a key which reprensente the string name which will be displayed +- The item should have a dictionnary reprensenting the coupled permissions, the keys (should be two keys) are `read` and `edit` or other keys like `no-admin` and `admin` +- The item should look like `item_key": {"read": "added_permission", "edit": "coupled_permission"}` +- Add translations for all the keys(`item_key, read and edit`) and the tooltip message of the principal key(`item_key`) + +## 5. Make and run migration `docker compose run --rm iaso manage makemigrations && docker compose run --rm iaso manage migrate` -## 5. Add the permission in the front-end +## 6. Add the permission in the front-end - Go to `/hat/assets/js/apps/Iaso/utils/permissions.ts`. Add and export a constant with the permission key, in a similar way as what was done for the backend in step 1. - When using the permission in the front-end: import the constant, don't write the key in a string. -## 6. Add translations in the front-end +## 7. Add translations in the front-end - Add a translation for the permission, and its tooltip in `permissionMessages.ts`. The tooltip key should have the format: `_tooltip` to enable the component to recognize and translate it. - Add corresponding translations in `en.json` and `fr.json` -## 7. Add translation for new module (if applicable) +## 8. Add translation for new module (if applicable) - Go to `/hat/assets/js/apps/Iaso/domains/modules/messages.ts` - Add translation for the new module. The translation key should follow the pattern: `iaso.module.' 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 af8c9af736..87e0c48f13 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/en.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/en.json @@ -1012,6 +1012,37 @@ "iaso.permissions.polio_vaccine_authorizations_admin": "Polio Vaccine Authorizations: Admin", "iaso.permissions.polio_vaccine_authorizations_read_only": "Polio Vaccine Authorizations: Read Only", "iaso.permissions.projects": "Projects", + "iaso.permissions.readEdit.admin": "Admin", + "iaso.permissions.readEdit.entity_duplicate_permissions": "Entity duplicates", + "iaso.permissions.readEdit.no_admin": "No-Admin", + "iaso.permissions.readEdit.org_unit_permissions": "Organisation units management", + "iaso.permissions.readEdit.page_permissions": "Web embedded links management", + "iaso.permissions.readEdit.planning_permissions": "Planning", + "iaso.permissions.readEdit.polio_budget_permissions": "Polio budget", + "iaso.permissions.readEdit.polio_chronogram_permissions": "Polio chronogram", + "iaso.permissions.readEdit.polio_vaccine_authorization_permissions": "Polio Vaccine Authorizations", + "iaso.permissions.readEdit.polio_vaccine_stock_management_permissions": "Polio vaccine stock management", + "iaso.permissions.readEdit.polio_vaccine_supply_chain_permissions": "Polio vaccine supply chain", + "iaso.permissions.readEdit.read": "Read", + "iaso.permissions.readEdit.registry_permissions": "Registry", + "iaso.permissions.readEdit.source_permissions": "Geo data sources", + "iaso.permissions.readEdit.submission_permissions": "Forms and submissions", + "iaso.permissions.readEdit.tooltip.entity_duplicate_permissions": "View (without the possibility to merge them) and edit entity duplicates", + "iaso.permissions.readEdit.tooltip.org_unit_permissions": "Manage organisation units and pyramids, including uploading of geo data (GPS coordinates and shapes), and groups", + "iaso.permissions.readEdit.tooltip.page_permissions": "External links management: View and edit an external link", + "iaso.permissions.readEdit.tooltip.planning_permissions": "View and edit planning", + "iaso.permissions.readEdit.tooltip.polio_budget_permissions": "View budget approval process and take action as defined by your role in the process. Extra admin powers: Override any step in the process if needed.", + "iaso.permissions.readEdit.tooltip.polio_chronogram_permissions": "Manage polio chronogram - Read and Write - Restricted Write", + "iaso.permissions.readEdit.tooltip.polio_vaccine_authorization_permissions": "Admin and no-admin permission on managing polio vaccine authorizations", + "iaso.permissions.readEdit.tooltip.polio_vaccine_stock_management_permissions": "See summary of vaccine stock management, by country and vaccine. Edit and add vaccine stock management data", + "iaso.permissions.readEdit.tooltip.polio_vaccine_supply_chain_permissions": "See summary of vaccine supply chain, by country and vaccine. Edit and add supply chain data", + "iaso.permissions.readEdit.tooltip.registry_permissions": "View and edit summary view of data collected per organisation unit", + "iaso.permissions.readEdit.tooltip.source_permissions": "View and edit geo data sources", + "iaso.permissions.readEdit.tooltip.submission_permissions": "View and edit the forms submissions", + "iaso.permissions.readEdit.tooltip.user_permissions": "Managed (Edition rights limited to the users linked to the children org units of the current user.) and admin permissions on managing users of the account: create or edit users (user name, email, password, permissions/location/language/project/user role)", + "iaso.permissions.readEdit.user_managed": "Managed", + "iaso.permissions.readEdit.user_permissions": "User management", + "iaso.permissions.readEdit.write": "Write", "iaso.permissions.sources": "Geo data sources - Read only", "iaso.permissions.submissions": "Forms and submissions - Read only", "iaso.permissions.teams": "Teams management", 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 583abdd982..228484669a 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json @@ -320,7 +320,7 @@ "iaso.groups.sourceVersion": "Version de la source", "iaso.groups.update": "Mettre le groupe à jour", "iaso.groupsets.dialog.delete": "Etes-vous certain de vouloir effacer cet ensemble de groupe?", - "iaso.groupsets.dialog.deleteText": "Cette opération ne peut être annulée", + "iaso.groupsets.dialog.deleteText": "Cette opération ne peut-être annulée", "iaso.groupsets.groupBelonging": "Appartenance aux groupes", "iaso.groupsets.validation.field_required": "Ce champ est obligatoire", "iaso.hospital": "Hôpital", @@ -1012,6 +1012,37 @@ "iaso.permissions.polio_vaccine_authorizations_admin": "Validation d'authorisation de vaccins polio: Admin", "iaso.permissions.polio_vaccine_authorizations_read_only": "Validation d'authorisation de vaccins polio: Lecture seule", "iaso.permissions.projects": "Projets", + "iaso.permissions.readEdit.admin": "Admin", + "iaso.permissions.readEdit.entity_duplicate_permissions": "Doublons d’entités", + "iaso.permissions.readEdit.no_admin": "Non-Admin", + "iaso.permissions.readEdit.org_unit_permissions": "Gestion des unités d’organisation", + "iaso.permissions.readEdit.page_permissions": "Gestion des liens intégrés web", + "iaso.permissions.readEdit.planning_permissions": "Planning", + "iaso.permissions.readEdit.polio_budget_permissions": "Budget Polio", + "iaso.permissions.readEdit.polio_chronogram_permissions": "Chronogramme Polio", + "iaso.permissions.readEdit.polio_vaccine_authorization_permissions": "Validation d'authorisation de vaccins polio", + "iaso.permissions.readEdit.polio_vaccine_stock_management_permissions": "Polio gestion des stocks de vaccins", + "iaso.permissions.readEdit.polio_vaccine_supply_chain_permissions": "Polio: chaîne d'approvisionnement", + "iaso.permissions.readEdit.read": "Lecture", + "iaso.permissions.readEdit.registry_permissions": "Registre", + "iaso.permissions.readEdit.source_permissions": "Sources de données géo", + "iaso.permissions.readEdit.submission_permissions": "Formulaires et soumissions", + "iaso.permissions.readEdit.tooltip.entity_duplicate_permissions": "Lecture et écriture des doublons d’entités - décision de fusionner ou non des entités similaires", + "iaso.permissions.readEdit.tooltip.org_unit_permissions": "Gestion des unités d’organisation et de la pyramide, y compris le chargement de données géographiques (points GPS et contours) et la gestion des groupes", + "iaso.permissions.readEdit.tooltip.page_permissions": "Gestion des liens externes : créer ou modifier un lien externe", + "iaso.permissions.readEdit.tooltip.planning_permissions": "Lecture et écriture des plannings", + "iaso.permissions.readEdit.tooltip.polio_budget_permissions": "Consultation du processus d’approbation du budget and actions possibles en fonction du rôle assigné à l’utilisateur par équipe. Pouvoirs Admin: si besoin, imposer une étape du budget indépendamment des conditions pré-établies", + "iaso.permissions.readEdit.tooltip.polio_chronogram_permissions": "Gestion des chronogrammes Polio - Lecture et écriture - Écriture restreinte", + "iaso.permissions.readEdit.tooltip.polio_vaccine_authorization_permissions": "Permissions admin et non admin pour la gestion des autorisations de vaccins polio", + "iaso.permissions.readEdit.tooltip.polio_vaccine_stock_management_permissions": "Voir le résumé de données de gestion des stocks de vaccins, par pays et par vaccin. Editer et ajouter des données de gestion des stocks de vaccins", + "iaso.permissions.readEdit.tooltip.polio_vaccine_supply_chain_permissions": "Voir le résumé des données de chaîne d'approvisionnement. Editer et ajouter des données", + "iaso.permissions.readEdit.tooltip.registry_permissions": "Lecture et écriture du résumé des données collectées par unité d’organisation", + "iaso.permissions.readEdit.tooltip.source_permissions": "Gestion des sources de données géographiques: créer ou éditer (nom, description, projet(s), version par défaut, liens DHIS2)", + "iaso.permissions.readEdit.tooltip.submission_permissions": "Lecture et écriture des soumissions de formulaires", + "iaso.permissions.readEdit.tooltip.user_permissions": "Permissions gérées (Pouvoirs d'édition limités aux utilisateurs placés en aval dans la pyramide (unités d’organisation enfants seulement)) et les permissions admin pour la gestion des utilisateurs du compte: création et édition (nom d’utilisateur, email, mot de passe, permissions/lieu attaché/langue/projet/rôle)", + "iaso.permissions.readEdit.user_managed": "Géré", + "iaso.permissions.readEdit.user_permissions": "Gestion des utilisateurs", + "iaso.permissions.readEdit.write": "Ecriture", "iaso.permissions.sources": "Sources de données géo - Lecture seule", "iaso.permissions.submissions": "Formulaires et soumissions - Lecture seule", "iaso.permissions.teams": "Gestion des équipes", diff --git a/hat/assets/js/apps/Iaso/domains/userRoles/components/CreateEditUserRole.tsx b/hat/assets/js/apps/Iaso/domains/userRoles/components/CreateEditUserRole.tsx index 1b6b122961..981be34f8c 100644 --- a/hat/assets/js/apps/Iaso/domains/userRoles/components/CreateEditUserRole.tsx +++ b/hat/assets/js/apps/Iaso/domains/userRoles/components/CreateEditUserRole.tsx @@ -19,10 +19,10 @@ import { useTranslatedErrors, } from '../../../libs/validation'; import InputComponent from '../../../components/forms/InputComponent'; -import { PermissionsSwitches } from './PermissionsSwitches'; -import { Permission } from '../types/userRoles'; +import { PermissionsAttribution } from './PermissionsAttribution'; import { EditIconButton } from '../../../components/Buttons/EditIconButton'; import UserRoleDialogInfoComponent from './UserRoleDialogInfoComponent'; +import { Permission } from '../types/userRoles'; type ModalMode = 'create' | 'edit'; type Props = Partial & { @@ -130,7 +130,7 @@ export const CreateEditUserRole: FunctionComponent = ({ onCancel={() => { resetForm(); }} - maxWidth="sm" + maxWidth="md" cancelMessage={MESSAGES.cancel} confirmMessage={MESSAGES.save} open={open} @@ -149,7 +149,7 @@ export const CreateEditUserRole: FunctionComponent = ({ label={MESSAGES.name} required /> - { handlePermissionsChange(newPermissions); diff --git a/hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsSwitches.tsx b/hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsAttribution.tsx similarity index 85% rename from hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsSwitches.tsx rename to hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsAttribution.tsx index 7f04b7316c..d5c61bfb2e 100644 --- a/hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsSwitches.tsx +++ b/hat/assets/js/apps/Iaso/domains/userRoles/components/PermissionsAttribution.tsx @@ -31,7 +31,7 @@ const styles = theme => ({ const useStyles = makeStyles(styles); type Props = { - userRolePermissions: Permission[]; + userRolePermissions: (string | Permission)[]; handleChange: (newValue: any) => void; }; @@ -40,37 +40,39 @@ type Row = { codename?: string; group?: boolean; id?: number; + readEdit?: { read: string; edit: string }[]; }; -export const PermissionsSwitches: React.FunctionComponent = ({ +export const PermissionsAttribution: React.FunctionComponent = ({ userRolePermissions, handleChange, }) => { const { formatMessage } = useSafeIntl(); const classes = useStyles(); - const { data, isLoading } = useSnackQuery<{ permissions: Permission[] }>( + const { data, isLoading } = useSnackQuery<{ permissions: string[] }>( ['grouped_permissions'], () => getRequest('/api/permissions/grouped_permissions/'), MESSAGES.fetchPermissionsError, ); const setPermissions = useCallback( - (permission: Permission, isChecked: boolean) => { + (permission: string, isChecked: boolean) => { const newUserRolePerms = [...userRolePermissions]; if (!isChecked) { const permIndex = newUserRolePerms.findIndex(item => { - return item.codename === permission.codename; + return item === permission; }); newUserRolePerms.splice(permIndex, 1); - } else { - newUserRolePerms.push({ - id: permission.id, - codename: permission.codename, - name: permission.name, + } else if (Array.isArray(permission)) { + permission.forEach(code => { + newUserRolePerms.push(code); }); + } else { + newUserRolePerms.push(permission); } handleChange(newUserRolePerms); }, + [handleChange, userRolePermissions], ); @@ -109,6 +111,9 @@ export const PermissionsSwitches: React.FunctionComponent = ({ row.id = permission.id; row.codename = permission.codename; row.name = getPermissionLabel(permission.codename); + if (permission.read_edit) { + row.readEdit = permission.read_edit; + } grouped_permissions.push(row); }); }); diff --git a/hat/assets/js/apps/Iaso/domains/userRoles/config.tsx b/hat/assets/js/apps/Iaso/domains/userRoles/config.tsx index 0d3b19f802..77b3879d06 100644 --- a/hat/assets/js/apps/Iaso/domains/userRoles/config.tsx +++ b/hat/assets/js/apps/Iaso/domains/userRoles/config.tsx @@ -2,7 +2,7 @@ import { Column, IntlFormatMessage, useSafeIntl } from 'bluesquare-components'; import React, { ReactElement, useMemo } from 'react'; import { DateTimeCell } from '../../components/Cells/DateTimeCell'; import DeleteDialog from '../../components/dialogs/DeleteDialogComponent'; -import PermissionSwitch from '../users/components/PermissionSwitch'; +import PermissionCheckBoxes from '../users/components/PermissionCheckBoxes'; import PermissionTooltip from '../users/components/PermissionTooltip'; import USER_MESSAGES from '../users/messages'; import { EditUserRoleDialog } from './components/CreateEditUserRole'; @@ -65,8 +65,8 @@ export const useGetUserRolesColumns = ( }; export const useUserPermissionColumns = ( - setPermissions: (permission: Permission, isChecked: boolean) => void, - userRolePermissions: Permission[], + setPermissions: (permission: string, isChecked: boolean) => void, + userRolePermissions: (string | Permission)[], ): Array => { const { formatMessage } = useSafeIntl(); return useMemo(() => { @@ -108,11 +108,11 @@ export const useUserPermissionColumns = ( sortable: false, Cell: settings => { return ( - ); diff --git a/hat/assets/js/apps/Iaso/domains/userRoles/hooks/requests/useSaveUserRole.ts b/hat/assets/js/apps/Iaso/domains/userRoles/hooks/requests/useSaveUserRole.ts index eceabfe31c..06e8eb63b0 100644 --- a/hat/assets/js/apps/Iaso/domains/userRoles/hooks/requests/useSaveUserRole.ts +++ b/hat/assets/js/apps/Iaso/domains/userRoles/hooks/requests/useSaveUserRole.ts @@ -13,9 +13,7 @@ export type SaveUserRoleQuery = { const convertToApi = data => { const { permissions, ...converted } = data; if (!isEmpty(permissions)) { - converted.permissions = permissions.map( - permission => permission.codename, - ); + converted.permissions = permissions; } return converted; }; diff --git a/hat/assets/js/apps/Iaso/domains/userRoles/validation.ts b/hat/assets/js/apps/Iaso/domains/userRoles/validation.ts index 8cdefea39e..1b6e21b059 100644 --- a/hat/assets/js/apps/Iaso/domains/userRoles/validation.ts +++ b/hat/assets/js/apps/Iaso/domains/userRoles/validation.ts @@ -16,15 +16,7 @@ export const useUserRoleValidation = ( const schema = useMemo(() => { return object().shape({ name: string().nullable().required('requiredField'), - permissions: array() - .of( - object({ - id: number(), - name: string(), - codename: string(), - }), - ) - .test(apiValidator('permissions')), + permissions: array().of(string()).test(apiValidator('permissions')), }); }, [apiValidator]); return schema; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/Filters.js b/hat/assets/js/apps/Iaso/domains/users/components/Filters.js index e1e03bc85a..60a07e10a2 100644 --- a/hat/assets/js/apps/Iaso/domains/users/components/Filters.js +++ b/hat/assets/js/apps/Iaso/domains/users/components/Filters.js @@ -9,7 +9,7 @@ import { useSafeIntl, useRedirectTo, } from 'bluesquare-components'; -import MESSAGES from '../messages'; +import MESSAGES from '../messages.ts'; import { stringToBoolean } from '../../../utils/dataManipulation.ts'; import { OrgUnitTreeviewModal } from '../../orgUnits/components/TreeView/OrgUnitTreeviewModal.tsx'; import { useGetPermissionsDropDown } from '../hooks/useGetPermissionsDropdown.ts'; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckBoxes.tsx b/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckBoxes.tsx new file mode 100644 index 0000000000..4c0f016411 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckBoxes.tsx @@ -0,0 +1,103 @@ +/* eslint-disable no-unused-vars */ +import React, { useCallback } from 'react'; +import { Permission } from '../../userRoles/types/userRoles'; +import PermissionCheckbox from './PermissionCheckbox'; + +type Props = { + value: string | Permission; + codeName: string; + settings: any; + + setPermissions: ( + permission: (string | Permission) | (string | Permission)[], + checked: boolean, + ) => void; + permissions: (string | Permission)[]; +}; + +const PermissionCheckBoxes: React.FunctionComponent = ({ + value, + codeName, + settings, + setPermissions, + permissions, +}) => { + const { original } = settings.row; + + const handleCheckboxChange = useCallback( + ( + permission: string | Permission, + checked: boolean, + keyPermission: string | null, + checkBoxKeys: string[] = [], + checkBoxs: any = undefined, + ) => { + const permissionsToCheckOrUncheck = [permission]; + if ( + checkBoxKeys.length > 1 && + checkBoxKeys[1] === keyPermission && + checked + ) { + permissionsToCheckOrUncheck.push(checkBoxs[checkBoxKeys[0]]); + setPermissions(permissionsToCheckOrUncheck, checked); + } else { + setPermissions(permission, checked); + } + }, + [setPermissions], + ); + + const isChecked = (permissionCode: string) => { + return Boolean(permissions.find(up => up === permissionCode)); + }; + + if (!original.group) { + if (original.readEdit) { + const checkBoxKeys = Object.keys(original.readEdit); + return ( +
+ {Object.entries(original.readEdit).map( + ([permissionKey]) => { + const permissionCode = + original.readEdit[permissionKey]; + + return ( + + + + ); + }, + )} +
+ ); + } + + return ( +
+ +
+ ); + } + + return null; +}; + +export default PermissionCheckBoxes; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckbox.tsx b/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckbox.tsx new file mode 100644 index 0000000000..b4ff841fbb --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/users/components/PermissionCheckbox.tsx @@ -0,0 +1,69 @@ +/* eslint-disable no-unused-vars */ +import React from 'react'; +import InputComponent from '../../../components/forms/InputComponent'; +import PERMISSIONS_MESSAGES from '../permissionsMessages'; +import { Permission } from '../../userRoles/types/userRoles'; + +type PermissionCheckboxProps = { + permissionCode: string; + permission: string | Permission; + keyPermission: string | null; + checkBoxKeys?: string[]; + checkBoxs?: any; + isChecked: (permissionCode: string) => boolean; + handleCheckboxChange: ( + permission: string | Permission, + checked: boolean, + keyPermission: string | null, + checkBoxKeys?: string[], + checkBoxs?: any, + ) => void; + allPermissions: (string | Permission)[]; +}; + +const PermissionCheckbox: React.FunctionComponent = ({ + permissionCode, + permission, + keyPermission, + checkBoxKeys = [], + checkBoxs = undefined, + isChecked, + handleCheckboxChange, + allPermissions, +}) => { + const checkBoxLabel = + keyPermission !== null + ? { label: PERMISSIONS_MESSAGES[keyPermission] } + : { labelString: '' }; + + const permissionsChecked = allPermissions; + + const disabled = + checkBoxKeys.length > 1 && + keyPermission === checkBoxKeys[0] && + permissionsChecked.includes(checkBoxs[checkBoxKeys[1]]); + + return ( + + handleCheckboxChange( + permission, + checked, + keyPermission, + checkBoxKeys, + checkBoxs, + ) + } + // eslint-disable-next-line react/jsx-props-no-spreading + {...checkBoxLabel} + dataTestId="permission-checkbox" + withMarginTop={false} + disabled={disabled} + /> + ); +}; + +export default PermissionCheckbox; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/PermissionSwitch.tsx b/hat/assets/js/apps/Iaso/domains/users/components/PermissionSwitch.tsx deleted file mode 100644 index 717259de21..0000000000 --- a/hat/assets/js/apps/Iaso/domains/users/components/PermissionSwitch.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Switch } from '@mui/material'; -import React from 'react'; -import { Permission } from '../../userRoles/types/userRoles'; - -type Props = { - value: string | Permission; - codeName: string; - settings: any; - setPermissions: (permission: string | Permission, checked: boolean) => void; - permissions: Permission[]; -}; - -const PermissionSwitch: React.FunctionComponent = ({ - value, - codeName, - settings, - setPermissions, - permissions, -}) => { - if (!settings.row.original.group) { - return ( - { - return typeof up === 'string' - ? up === settings.row.original[codeName] - : up.codename === settings.row.original[codeName]; - }), - )} - onChange={e => { - setPermissions(value, e.target.checked); - }} - name={settings.row.original[codeName]} - color="primary" - /> - ); - } - return null; -}; - -export default PermissionSwitch; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/PermissionsSwitches.tsx b/hat/assets/js/apps/Iaso/domains/users/components/PermissionsAttribution.tsx similarity index 93% rename from hat/assets/js/apps/Iaso/domains/users/components/PermissionsSwitches.tsx rename to hat/assets/js/apps/Iaso/domains/users/components/PermissionsAttribution.tsx index 41f32ca801..8ca3b893c7 100644 --- a/hat/assets/js/apps/Iaso/domains/users/components/PermissionsSwitches.tsx +++ b/hat/assets/js/apps/Iaso/domains/users/components/PermissionsAttribution.tsx @@ -13,7 +13,6 @@ import { useSnackQuery } from '../../../libs/apiHooks'; import * as Permissions from '../../../utils/permissions'; import { useCurrentUser } from '../../../utils/usersUtils'; import { useGetUserRolesDropDown } from '../../userRoles/hooks/requests/useGetUserRoles'; -import { Permission } from '../../userRoles/types/userRoles'; import { useUserPermissionColumns } from '../config'; import { useGetUserPermissions } from '../hooks/useGetUserPermissions'; import MESSAGES from '../messages'; @@ -57,10 +56,10 @@ type Props = { }; type PermissionResult = { - permissions: Permission[]; + permissions: string[]; }; -const PermissionsSwitches: React.FunctionComponent = ({ +const PermissionsAttribution: React.FunctionComponent = ({ isSuperUser, currentUser, handleChange, @@ -76,11 +75,15 @@ const PermissionsSwitches: React.FunctionComponent = ({ options: { enabled: !isSuperUser }, }); const setPermissions = useCallback( - (codeName: string, isChecked: boolean) => { + (codeName: string | string[], isChecked: boolean) => { const newUserPerms = [...currentUser.user_permissions.value]; if (!isChecked) { const permIndex = newUserPerms.indexOf(codeName); newUserPerms.splice(permIndex, 1); + } else if (Array.isArray(codeName)) { + codeName.forEach(code => { + newUserPerms.push(code); + }); } else { newUserPerms.push(codeName); } @@ -121,6 +124,7 @@ const PermissionsSwitches: React.FunctionComponent = ({ const newUserRoles = value ? value.split(',').map(userRoleId => parseInt(userRoleId, 10)) : []; + setFieldValue('user_roles', newUserRoles); }, [setFieldValue], @@ -174,8 +178,8 @@ const PermissionsSwitches: React.FunctionComponent = ({ ); }; -PermissionsSwitches.defaultProps = { +PermissionsAttribution.defaultProps = { isSuperUser: false, }; -export default PermissionsSwitches; +export default PermissionsAttribution; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/UserRolePermissions.tsx b/hat/assets/js/apps/Iaso/domains/users/components/UserRolePermissions.tsx new file mode 100644 index 0000000000..60493ed610 --- /dev/null +++ b/hat/assets/js/apps/Iaso/domains/users/components/UserRolePermissions.tsx @@ -0,0 +1,68 @@ +import { Stack, Typography } from '@mui/material'; +import React, { FunctionComponent } from 'react'; +import { + CheckCircleOutlineOutlined as CheckedIcon, + HighlightOffOutlined as NotCheckedIcon, +} from '@mui/icons-material'; +import { useSafeIntl } from 'bluesquare-components'; +import PERMISSIONS_MESSAGES from '../permissionsMessages'; +import { Permission } from '../../userRoles/types/userRoles'; + +type Props = { + original: any; + userRolepermissions: (string | Permission)[]; +}; +export const UserRolePermissions: FunctionComponent = ({ + original, + userRolepermissions, +}) => { + const { formatMessage } = useSafeIntl(); + if (original.readEdit) { + const permissions: [string, string][] = Object.entries( + original.readEdit, + ); + return ( + + {permissions.map(([key, value]) => { + const hasPermission = userRolepermissions.includes(value); + return ( + + {hasPermission ? ( + + ) : ( + + )} + + {formatMessage(PERMISSIONS_MESSAGES[key])} + + + ); + })} + + ); + } + + const hasPermission = userRolepermissions.includes( + original.permissionCodeName, + ); + + return hasPermission ? ( + + ) : ( + + ); +}; diff --git a/hat/assets/js/apps/Iaso/domains/users/components/UsersDialog.tsx b/hat/assets/js/apps/Iaso/domains/users/components/UsersDialog.tsx index 57ebd2d053..1503ba29c4 100644 --- a/hat/assets/js/apps/Iaso/domains/users/components/UsersDialog.tsx +++ b/hat/assets/js/apps/Iaso/domains/users/components/UsersDialog.tsx @@ -20,7 +20,7 @@ import { EditIconButton } from '../../../components/Buttons/EditIconButton'; import { Profile, useCurrentUser } from '../../../utils/usersUtils'; import MESSAGES from '../messages'; import { InitialUserData } from '../types'; -import PermissionsSwitches from './PermissionsSwitches'; +import PermissionsAttribution from './PermissionsAttribution'; import UsersInfos from './UsersInfos'; import UsersLocations from './UsersLocations'; import { WarningModal } from './WarningModal/WarningModal'; @@ -231,7 +231,7 @@ const UserDialogComponent: FunctionComponent = ({ /> {tab === 'permissions' && ( - diff --git a/hat/assets/js/apps/Iaso/domains/users/components/useInitialUser.ts b/hat/assets/js/apps/Iaso/domains/users/components/useInitialUser.ts index cfdc5f887a..7dcbbda79d 100644 --- a/hat/assets/js/apps/Iaso/domains/users/components/useInitialUser.ts +++ b/hat/assets/js/apps/Iaso/domains/users/components/useInitialUser.ts @@ -129,9 +129,7 @@ export const useInitialUser = ( .map(userRole => { const role = { ...(userRole.original as UserRole), - permissions: userRole.original?.permissions.map( - perm => perm.codename, - ), + permissions: userRole.original?.permissions, }; return role; }); diff --git a/hat/assets/js/apps/Iaso/domains/users/config.js b/hat/assets/js/apps/Iaso/domains/users/config.js index 21f9915183..1087959a5f 100644 --- a/hat/assets/js/apps/Iaso/domains/users/config.js +++ b/hat/assets/js/apps/Iaso/domains/users/config.js @@ -1,7 +1,3 @@ -import { - CheckCircleOutlineOutlined as CheckedIcon, - HighlightOffOutlined as NotCheckedIcon, -} from '@mui/icons-material'; import { textPlaceholder, useSafeIntl } from 'bluesquare-components'; import React, { useMemo } from 'react'; @@ -12,10 +8,11 @@ import MESSAGES from './messages.ts'; import { userHasOneOfPermissions } from './utils'; import * as Permission from '../../utils/permissions.ts'; -import PermissionSwitch from './components/PermissionSwitch.tsx'; +import PermissionCheckBoxes from './components/PermissionCheckBoxes.tsx'; import PermissionTooltip from './components/PermissionTooltip.tsx'; import PERMISSIONS_GROUPS_MESSAGES from './permissionsGroupsMessages.ts'; import { DisplayIfUserHasPerm } from '../../components/DisplayIfUserHasPerm.tsx'; +import { UserRolePermissions } from './components/UserRolePermissions.tsx'; export const usersTableColumns = ({ formatMessage, @@ -157,9 +154,10 @@ export const useUserPermissionColumns = ({ setPermissions, currentUser }) => { id: 'userPermission', accessor: 'userPermission', sortable: false, + align: 'center', Cell: settings => { return ( - { width: 50, Cell: settings => { if (!settings.row.original.group) { - if ( - role.permissions.find( - permission => - permission === - settings.row.original.permissionCodeName, - ) - ) { - return ; - } - return ; + return ( + + ); } return ''; }, diff --git a/hat/assets/js/apps/Iaso/domains/users/hooks/useGetUserPermissions.tsx b/hat/assets/js/apps/Iaso/domains/users/hooks/useGetUserPermissions.tsx index 9c1a98b2d0..7c62407ac0 100644 --- a/hat/assets/js/apps/Iaso/domains/users/hooks/useGetUserPermissions.tsx +++ b/hat/assets/js/apps/Iaso/domains/users/hooks/useGetUserPermissions.tsx @@ -1,6 +1,5 @@ import { useSafeIntl } from 'bluesquare-components'; import { useCallback, useMemo } from 'react'; -import { Permission } from '../../userRoles/types/userRoles'; import PERMISSIONS_MESSAGES from '../permissionsMessages'; type Row = { @@ -41,6 +40,9 @@ export const useGetUserPermissions = ( row.permission = getPermissionLabel(p.codename); row.userPermissions = userPermissions; row.permissionCodeName = p.codename; + if (p.read_edit) { + row.readEdit = p.read_edit; + } data.push(row); }); @@ -50,7 +52,7 @@ export const useGetUserPermissions = ( }; type SortProps = { - allPermissions: Permission[]; + allPermissions: string[]; getPermissionLabel: (codename: string) => string; }; diff --git a/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts b/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts index 4182931b71..9a65682c9e 100644 --- a/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts +++ b/hat/assets/js/apps/Iaso/domains/users/permissionsMessages.ts @@ -595,6 +595,140 @@ const PERMISSIONS_MESSAGES = defineMessages({ id: 'iaso.permissions.tooltip.iaso_polio_chronogram_restricted_write', defaultMessage: 'Manage polio chronogram - Restricted Write', }, + read: { + id: 'iaso.permissions.readEdit.read', + defaultMessage: 'Read', + }, + write: { + id: 'iaso.permissions.readEdit.write', + defaultMessage: 'Write', + }, + no_admin: { + id: 'iaso.permissions.readEdit.no_admin', + defaultMessage: 'No-admin', + }, + admin: { + id: 'iaso.permissions.readEdit.admin', + defaultMessage: 'Admin', + }, + user_managed: { + id: 'iaso.permissions.readEdit.user_managed', + defaultMessage: 'Managed', + }, + iaso_submission_permissions: { + id: 'iaso.permissions.readEdit.submission_permissions', + defaultMessage: 'Forms and submissions', + }, + iaso_submission_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.submission_permissions', + defaultMessage: 'View and edit the forms submissions', + }, + iaso_org_unit_permissions: { + id: 'iaso.permissions.readEdit.org_unit_permissions', + defaultMessage: 'Organisation units management', + }, + iaso_org_unit_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.org_unit_permissions', + defaultMessage: + 'Manage organisation units and pyramids, including uploading of geo data (GPS coordinates and shapes), and groups', + }, + iaso_registry_permissions: { + id: 'iaso.permissions.readEdit.registry_permissions', + defaultMessage: 'Registry', + }, + iaso_registry_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.registry_permissions', + defaultMessage: + 'View and edit summary view of data collected per organisation unit', + }, + iaso_source_permissions: { + id: 'iaso.permissions.readEdit.source_permissions', + defaultMessage: 'Geo data sources', + }, + iaso_source_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.source_permissions', + defaultMessage: 'View and edit geo data sources', + }, + iaso_entity_duplicate_permissions: { + id: 'iaso.permissions.readEdit.entity_duplicate_permissions', + defaultMessage: 'Entity duplicates', + }, + iaso_entity_duplicate_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.entity_duplicate_permissions', + defaultMessage: + 'View (without the possibility to merge them) and edit entity duplicates', + }, + iaso_planning_permissions: { + id: 'iaso.permissions.readEdit.planning_permissions', + defaultMessage: 'Planning', + }, + iaso_planning_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.planning_permissions', + defaultMessage: 'View and edit planning', + }, + iaso_page_permissions: { + id: 'iaso.permissions.readEdit.page_permissions', + defaultMessage: 'Web embedded links management', + }, + iaso_page_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.page_permissions', + defaultMessage: + 'External links management: View and edit an external link', + }, + iaso_polio_budget_permissions: { + id: 'iaso.permissions.readEdit.polio_budget_permissions', + defaultMessage: 'Polio budget', + }, + iaso_polio_budget_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.polio_budget_permissions', + defaultMessage: + 'View budget approval process and take action as defined by your role in the process. Extra admin powers: Override any step in the process if needed.', + }, + iaso_polio_chronogram_permissions: { + id: 'iaso.permissions.readEdit.polio_chronogram_permissions', + defaultMessage: 'Polio chronogram', + }, + iaso_polio_chronogram_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.polio_chronogram_permissions', + defaultMessage: + 'Manage polio chronogram - Read and Write - Restricted Write', + }, + iaso_polio_vaccine_supply_chain_permissions: { + id: 'iaso.permissions.readEdit.polio_vaccine_supply_chain_permissions', + defaultMessage: 'Polio vaccine supply chain', + }, + iaso_polio_vaccine_supply_chain_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.polio_vaccine_supply_chain_permissions', + defaultMessage: + 'See summary of vaccine supply chain, by country and vaccine. Edit and add supply chain data', + }, + iaso_polio_vaccine_stock_management_permissions: { + id: 'iaso.permissions.readEdit.polio_vaccine_stock_management_permissions', + defaultMessage: 'Polio vaccine stock management', + }, + iaso_polio_vaccine_stock_management_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.polio_vaccine_stock_management_permissions', + defaultMessage: + 'See summary of vaccine stock management, by country and vaccine. Edit and add vaccine stock management data', + }, + iaso_polio_vaccine_authorization_permissions: { + id: 'iaso.permissions.readEdit.polio_vaccine_authorization_permissions', + defaultMessage: 'Polio Vaccine Authorizations', + }, + iaso_polio_vaccine_authorization_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.polio_vaccine_authorization_permissions', + defaultMessage: + 'Admin and no-admin permission on managing polio vaccine authorizations', + }, + iaso_user_permissions: { + id: 'iaso.permissions.readEdit.user_permissions', + defaultMessage: 'User management', + }, + iaso_user_permissions_tooltip: { + id: 'iaso.permissions.readEdit.tooltip.user_permissions', + defaultMessage: + 'Managed (Edition rights limited to the users linked to the children org units of the current user.) and admin permissions on managing users of the account: create or edit users (user name, email, password, permissions/location/language/project/user role)', + }, }); export default PERMISSIONS_MESSAGES; diff --git a/hat/assets/js/cypress/integration/03 - Users/list.spec.js b/hat/assets/js/cypress/integration/03 - Users/list.spec.js index d6c8695e64..8a28f6d0c1 100644 --- a/hat/assets/js/cypress/integration/03 - Users/list.spec.js +++ b/hat/assets/js/cypress/integration/03 - Users/list.spec.js @@ -216,7 +216,9 @@ describe('Users', () => { cy.get('.MuiDialogActions-root').find('button').first().click(); openDialogForUserIndex(2); cy.get('#user-dialog-tabs').find('button').eq(1).click(); - cy.get('#permission-checkbox-iaso_forms').should('be.checked'); + cy.get('#check-box-permission-checkbox-iaso_forms').should( + 'be.checked', + ); cy.get('#user-dialog-tabs').find('button').eq(2).click(); cy.get('.MuiTreeView-root').should( diff --git a/hat/menupermissions/constants.py b/hat/menupermissions/constants.py index 7523a7795b..80ccdcf255 100644 --- a/hat/menupermissions/constants.py +++ b/hat/menupermissions/constants.py @@ -228,3 +228,34 @@ "iaso_mobile_app_offline_setup", ], } + +READ_EDIT_PERMISSIONS = { + "iaso_submission_permissions": {"read": "iaso_submissions", "write": "iaso_update_submission"}, + "iaso_org_unit_permissions": {"read": "iaso_org_units_read", "write": "iaso_org_units"}, + "iaso_registry_permissions": {"read": "iaso_registry_read", "write": "iaso_registry_write"}, + "iaso_source_permissions": {"read": "iaso_sources", "write": "iaso_write_sources"}, + "iaso_entity_duplicate_permissions": { + "read": "iaso_entity_duplicates_read", + "write": "iaso_entity_duplicates_write", + }, + "iaso_planning_permissions": {"read": "iaso_planning_read", "write": "iaso_planning_write"}, + "iaso_page_permissions": {"read": "iaso_pages", "write": "iaso_page_write"}, + "iaso_polio_budget_permissions": {"read": "iaso_polio_budget", "write": "iaso_polio_budget_admin"}, + "iaso_polio_chronogram_permissions": { + "read": "iaso_polio_chronogram_restricted_write", + "write": "iaso_polio_chronogram", + }, + "iaso_polio_vaccine_supply_chain_permissions": { + "read": "iaso_polio_vaccine_supply_chain_read", + "write": "iaso_polio_vaccine_supply_chain_write", + }, + "iaso_polio_vaccine_stock_management_permissions": { + "read": "iaso_polio_vaccine_stock_management_read", + "write": "iaso_polio_vaccine_stock_management_write", + }, + "iaso_polio_vaccine_authorization_permissions": { + "no_admin": "iaso_polio_vaccine_authorizations_read_only", + "admin": "iaso_polio_vaccine_authorizations_admin", + }, + "iaso_user_permissions": {"user_managed": "iaso_users_managed", "admin": "iaso_users"}, +} diff --git a/iaso/api/permissions.py b/iaso/api/permissions.py index a8b59db483..3877c54352 100644 --- a/iaso/api/permissions.py +++ b/iaso/api/permissions.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib.auth.models import Permission from django.utils.translation import gettext as _ -from hat.menupermissions.constants import PERMISSIONS_PRESENTATION +from hat.menupermissions.constants import PERMISSIONS_PRESENTATION, READ_EDIT_PERMISSIONS from iaso.utils.module_permissions import account_module_permissions from rest_framework import viewsets, permissions from rest_framework.response import Response @@ -51,13 +51,47 @@ def get_grouped_permissions(self, permissions_queryset): def get_permissions_for_group(self, permissions_queryset, permission_codenames): filtered_permissions = permissions_queryset.filter(codename__in=permission_codenames) - if not filtered_permissions: - return None - return [ - {"id": permission.id, "name": _(permission.name), "codename": permission.codename} - for permission in filtered_permissions - ] + if not filtered_permissions: + return [] + + permissions = [] + read_edit_permissions_map = READ_EDIT_PERMISSIONS.keys() + + processed_codenames = set() + + for permission in filtered_permissions: + matching_key = next( + ( + key + for key in read_edit_permissions_map + if permission.codename in READ_EDIT_PERMISSIONS[key].values() + ), + None, + ) + + if matching_key: + if permission.codename not in processed_codenames: + read_edit_data = READ_EDIT_PERMISSIONS[matching_key] + combined_permissions = { + "id": permission.id, + "name": _(matching_key), + "codename": matching_key, + "read_edit": read_edit_data, + } + permissions.append(combined_permissions) + processed_codenames.update(read_edit_data.values()) + + else: + permissions.append( + { + "id": permission.id, + "name": _(permission.name), + "codename": permission.codename, + } + ) + + return permissions def queryset(self, request): if request.user.has_perm(p.USERS_ADMIN) or request.user.has_perm(p.USERS_MANAGED): diff --git a/iaso/api/user_roles.py b/iaso/api/user_roles.py index 4bd27ab5b3..039605fdb5 100644 --- a/iaso/api/user_roles.py +++ b/iaso/api/user_roles.py @@ -47,7 +47,7 @@ def remove_prefix_from_str(self, str, prefix): return str def get_permissions(self, obj): - return PermissionSerializer(obj.group.permissions, many=True).data + return [permission["codename"] for permission in PermissionSerializer(obj.group.permissions, many=True).data] def create(self, validated_data): account = self.context["request"].user.iaso_profile.account diff --git a/iaso/tests/api/test_permissions.py b/iaso/tests/api/test_permissions.py index 61d5230151..c2a278ff81 100644 --- a/iaso/tests/api/test_permissions.py +++ b/iaso/tests/api/test_permissions.py @@ -56,6 +56,6 @@ def test_get_all_grouped_permissions(self): self.assertJSONResponse(response, 200) self.assertEqual(list(response.json()["permissions"].keys()), ["org_units", "admin"]) self.assertTrue( - set([permission["codename"] for permission in response.json()["permissions"]["org_units"]]) - <= set(PERMISSIONS_PRESENTATION["org_units"]), + [permission["codename"] for permission in response.json()["permissions"]["org_units"]] + <= PERMISSIONS_PRESENTATION["org_units"], ) diff --git a/iaso/tests/api/test_user_roles.py b/iaso/tests/api/test_user_roles.py index b920f1a5e8..9c2be70b30 100644 --- a/iaso/tests/api/test_user_roles.py +++ b/iaso/tests/api/test_user_roles.py @@ -140,7 +140,7 @@ def test_partial_update_permissions_modification(self): r = self.assertJSONResponse(response, 200) self.assertEqual( - [r["permissions"][0]["codename"], r["permissions"][1]["codename"]], + [r["permissions"][0], r["permissions"][1]], [self.permission1.codename, self.permission2.codename], )