Skip to content

Commit

Permalink
MAUI-1267: Allow deleting entities via the Admin Panel (beyondessenti…
Browse files Browse the repository at this point in the history
  • Loading branch information
rohan-bes authored Oct 11, 2022
1 parent 3c376b0 commit 0899601
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 28 deletions.
14 changes: 11 additions & 3 deletions packages/admin-panel/src/pages/resources/EntitiesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import { ResourcePage } from './ResourcePage';
import { SURVEY_RESPONSE_COLUMNS, ANSWER_COLUMNS } from './SurveyResponsesPage';

const ENTITY_ENDPOINT = 'entities';
const ENTITIES_ENDPOINT = 'entities';

export const ENTITIES_COLUMNS = [
{ source: 'id', show: false },
Expand Down Expand Up @@ -38,7 +38,7 @@ const FIELDS = [
source: 'id',
type: 'edit',
actionConfig: {
editEndpoint: ENTITY_ENDPOINT,
editEndpoint: ENTITIES_ENDPOINT,
title: 'Edit Entity',
fields: [
{
Expand All @@ -48,6 +48,14 @@ const FIELDS = [
],
},
},
{
Header: 'Delete',
source: 'id',
type: 'delete',
actionConfig: {
endpoint: ENTITIES_ENDPOINT,
},
},
];

const EXPANSION_CONFIG = [
Expand Down Expand Up @@ -95,7 +103,7 @@ const IMPORT_CONFIG = {
export const EntitiesPage = ({ getHeaderEl }) => (
<ResourcePage
title="Entities"
endpoint={ENTITY_ENDPOINT}
endpoint={ENTITIES_ENDPOINT}
columns={FIELDS}
expansionTabs={EXPANSION_CONFIG}
importConfig={IMPORT_CONFIG}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import {
constructRecordExistsWithId,
DatabaseError,
respond,
} from '@tupaia/utils';
import { constructRecordExistsWithId, DatabaseError, respond } from '@tupaia/utils';
import { CRUDHandler } from '../CRUDHandler';

/**
Expand Down
51 changes: 51 additions & 0 deletions packages/central-server/src/apiV2/entities/DeleteEntity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { BESAdminDeleteHandler } from '../DeleteHandler';

const UNDELETEABLE_ENTITY_TYPES = ['project', 'country'];

export class DeleteEntity extends BESAdminDeleteHandler {
async deleteRecord() {
const entity = await this.resourceModel.findById(this.recordId);
if (UNDELETEABLE_ENTITY_TYPES.includes(entity.type)) {
throw new Error(`Cannot delete an entity of type: ${entity.type}`);
}

// Check for existing survey responses (they should be cleaned up first)
const surveyResponsesForEntity = await this.models.surveyResponse.find({
entity_id: entity.id,
});
if (surveyResponsesForEntity.length > 0) {
throw new Error(
`There are still survey responses for this entity, please clean them up before deleting this entity`,
);
}

// Check for children (they should be given a new parent first)
const hierarchies = await this.models.entityHierarchy.all();
const hierarchiesWhereEntityHasChildren = [];
for (let i = 0; i < hierarchies.length; i++) {
const hierarchy = hierarchies[i];
const children = await entity.getChildren(hierarchy.id);
if (children.length > 0) {
hierarchiesWhereEntityHasChildren.push(hierarchy);
}
}

if (hierarchiesWhereEntityHasChildren.length > 0) {
throw new Error(
`This entity still has children in the following hierarchies [${hierarchiesWhereEntityHasChildren.map(
hierarchy => hierarchy.name,
)}], please delete them or re-import them with a new parent before deleting this entity`,
);
}

// Delete any entity_relations where this entity is the leaf node, as they prevent deleting the entity otherwise
await this.models.entityRelation.delete({ child_id: this.recordId });

await super.deleteRecord();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,21 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { GETHandler } from './GETHandler';
import { assertAnyPermissions, assertBESAdminAccess, hasBESAdminAccess } from '../permissions';
import { mergeFilter } from './utilities';
import { assertCountryPermissions } from './GETCountries';

export const assertEntityPermissions = async (accessPolicy, models, entityId) => {
const entity = await models.entity.findById(entityId);
if (!entity) {
throw new Error(`No entity exists with id ${entityId}`);
}
if (!accessPolicy.allows(entity.country_code)) {
throw new Error('You do not have permissions for this entity');
}
return true;
};
import { GETHandler } from '../GETHandler';
import { assertAnyPermissions, assertBESAdminAccess, hasBESAdminAccess } from '../../permissions';
import { mergeFilter } from '../utilities';
import { assertCountryPermissions } from '../GETCountries';
import { assertEntityPermissions } from './assertEntityPermissions';

export class GETEntities extends GETHandler {
permissionsFilteredInternally = true;

async findSingleRecord(entityId, options) {
const entityPermissionChecker = accessPolicy =>
assertEntityPermissions(accessPolicy, this.models, entityId);
await this.assertPermissions(assertAnyPermissions([assertBESAdminAccess, entityPermissionChecker]));
await this.assertPermissions(
assertAnyPermissions([assertBESAdminAccess, entityPermissionChecker]),
);

return super.findSingleRecord(entityId, options);
}
Expand All @@ -46,7 +38,9 @@ export class GETEntities extends GETHandler {
async getPermissionsViaParentFilter(criteria, options) {
const countryPermissionChecker = accessPolicy =>
assertCountryPermissions(accessPolicy, this.models, this.parentRecordId);
await this.assertPermissions(assertAnyPermissions([assertBESAdminAccess, countryPermissionChecker]));
await this.assertPermissions(
assertAnyPermissions([assertBESAdminAccess, countryPermissionChecker]),
);

const country = await this.models.country.findById(this.parentRecordId);
const dbConditions = { 'entity.country_code': country.code, ...criteria };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

export const assertEntityPermissions = async (accessPolicy, models, entityId) => {
const entity = await models.entity.findById(entityId);
if (!entity) {
throw new Error(`No entity exists with id ${entityId}`);
}
if (!accessPolicy.allows(entity.country_code)) {
throw new Error('You do not have permissions for this entity');
}
return true;
};
3 changes: 3 additions & 0 deletions packages/central-server/src/apiV2/entities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

export { DeleteEntity } from './DeleteEntity';
export { EditEntity } from './EditEntity';
export { GETEntities } from './GETEntities';
export { assertEntityPermissions } from './assertEntityPermissions';
4 changes: 2 additions & 2 deletions packages/central-server/src/apiV2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { GETDisasters } from './GETDisasters';
import { GETDataElements, EditDataElements, DeleteDataElements } from './dataElements';
import { GETDataGroups, EditDataGroups, DeleteDataGroups } from './dataGroups';
import { GETDataTables } from './dataTables';
import { GETEntities } from './GETEntities';
import { GETEntityTypes } from './GETEntityTypes';
import { GETFeedItems } from './GETFeedItems';
import { GETGeographicalAreas } from './GETGeographicalAreas';
Expand Down Expand Up @@ -90,7 +89,7 @@ import {
EditUserEntityPermissions,
GETUserEntityPermissions,
} from './userEntityPermissions';
import { EditEntity } from './entities';
import { EditEntity, GETEntities, DeleteEntity } from './entities';
import { EditAccessRequests, GETAccessRequests } from './accessRequests';
import { postChanges } from './postChanges';
import { changePassword } from './changePassword';
Expand Down Expand Up @@ -309,6 +308,7 @@ apiV2.delete('/surveyResponses/:parentRecordId/answers/:recordId', useRouteHandl
apiV2.delete('/dataElements/:recordId', useRouteHandler(DeleteDataElements));
apiV2.delete('/dataGroups/:recordId', useRouteHandler(DeleteDataGroups));
apiV2.delete('/disasters/:recordId', useRouteHandler(BESAdminDeleteHandler));
apiV2.delete('/entities/:recordId', useRouteHandler(DeleteEntity));
apiV2.delete('/feedItems/:recordId', useRouteHandler(BESAdminDeleteHandler));
apiV2.delete('/options/:recordId', useRouteHandler(DeleteOptions));
apiV2.delete('/optionSets/:recordId', useRouteHandler(DeleteOptionSets));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
createSurveyResponseDBFilter,
} from './assertSurveyResponsePermissions';
import { assertAnyPermissions, assertBESAdminAccess, hasBESAdminAccess } from '../../permissions';
import { assertEntityPermissions } from '../GETEntities';
import { assertEntityPermissions } from '../entities';
import { getQueryOptionsForColumns } from '../GETHandler/helpers';

/**
Expand Down

0 comments on commit 0899601

Please sign in to comment.