Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAUI-859: Enable Project Creation #4036

Merged
merged 20 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4b4765e
Add createConfig and projects endpoint
chris-pollard Jun 20, 2022
d554532
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Jul 15, 2022
5c3231e
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Jul 15, 2022
7619e5a
Add create project endpoint
chris-pollard Jul 21, 2022
5527409
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Jul 21, 2022
2669d32
Add rollback to createProject, write tests
chris-pollard Jul 28, 2022
7184b59
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Jul 28, 2022
786a9c9
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Jul 31, 2022
16acd3d
Add rollback transaction
chris-pollard Aug 11, 2022
54c32fd
Remove unused import
chris-pollard Aug 11, 2022
c2fd4de
Pull data from create functions
chris-pollard Aug 12, 2022
8308b06
Merge pull request #4099 from beyondessential/dev
Sima-BES Aug 19, 2022
9fa7f10
add validation for whitespace
chris-pollard Sep 5, 2022
be74a20
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Sep 8, 2022
35e7903
Adjust config based on changes
chris-pollard Sep 8, 2022
ade59f0
Merge pull request #4173 from beyondessential/dev
Sima-BES Sep 15, 2022
084129a
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Sep 16, 2022
a361375
Merge branch 'maui-859-enable-project-creation' of github.com:beyonde…
chris-pollard Sep 16, 2022
0bce588
Merge branch 'dev' into maui-859-enable-project-creation
chris-pollard Sep 16, 2022
09304a1
Fix error message
chris-pollard Sep 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions packages/admin-panel/src/pages/resources/ProjectsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ResourcePage } from './ResourcePage';
import { prettyArray } from '../../utilities';
import { ArrayFilter } from '../../table/columnTypes/columnFilters';

const PROJECTS_ENDPOINT = 'projects';

const FIELDS = [
{
Expand All @@ -22,12 +26,22 @@ const FIELDS = [
source: 'dashboard_group_name',
type: 'tooltip',
editConfig: {
optionsEndpoint: 'dashboardGroups',
optionsEndpoint: 'dashboards',
optionLabelKey: 'name',
optionValueKey: 'name',
sourceKey: 'dashboard_group_name',
},
},
{
Header: 'Map Overlay Code',
source: 'default_measure',
editConfig: {
optionsEndpoint: 'mapOverlays',
optionLabelKey: 'code',
optionValueKey: 'id',
sourceKey: 'default_measure',
},
},
{
Header: 'Permission Group',
source: 'permission_groups',
Expand All @@ -54,7 +68,9 @@ const FIELDS = [
Header: 'Config',
source: 'config',
type: 'jsonTooltip',
editConfig: { type: 'jsonEditor' },
editConfig: {
type: 'jsonEditor',
},
secondaryLabel: 'eg. { "tileSets": "osm,satellite,terrain", "permanentRegionLabels": true }',
},
{
Expand All @@ -64,6 +80,41 @@ const FIELDS = [
},
];

const NEW_PROJECT_COLUMNS = [
{
Header: 'Name',
source: 'name',
},
...FIELDS,

{
Header: 'Country Code/s',
source: 'country.code',
Filter: ArrayFilter,
Cell: ({ value }) => prettyArray(value),
editConfig: {
optionsEndpoint: 'countries',
optionLabelKey: 'country.code',
optionValueKey: 'country.id',
sourceKey: 'countries',
allowMultipleValues: true,
},
},
{
Header: 'Canonical Types (leave blank for default)',
source: 'entityType',
Filter: ArrayFilter,
Cell: ({ value }) => prettyArray(value),
editConfig: {
optionsEndpoint: 'entityTypes',
optionLabelKey: 'entityType',
optionValueKey: 'entityType',
sourceKey: 'entityTypes',
allowMultipleValues: true,
},
},
];

const COLUMNS = [
...FIELDS,
{
Expand All @@ -77,16 +128,25 @@ const COLUMNS = [
},
];

const CREATE_CONFIG = {
title: 'Create a new project',
actionConfig: {
editEndpoint: PROJECTS_ENDPOINT,
fields: NEW_PROJECT_COLUMNS,
},
};

const EDIT_CONFIG = {
title: 'Edit Project',
};

export const ProjectsPage = ({ getHeaderEl }) => (
<ResourcePage
title="Projects"
endpoint="projects"
endpoint={PROJECTS_ENDPOINT}
columns={COLUMNS}
editConfig={EDIT_CONFIG}
createConfig={CREATE_CONFIG}
getHeaderEl={getHeaderEl}
/>
);
Expand Down
21 changes: 21 additions & 0 deletions packages/central-server/src/apiV2/GETEntityTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { respond } from '@tupaia/utils';

export const getEntityTypes = async (req, res, next) => {
const { models } = req;
try {
const { types } = await models.entity;
const entityTypes = Object.values(types)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this endpoint should return all types, including world. There may be another frontend that wants to list it.

You can then either just leave World in the dropdown and leave it to users to ignore it or add this same filter in admin-panel instead (not sure how off the top of my head but should be possible).

Copy link
Contributor

@IgorNadj IgorNadj Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to still be the case in this PR, but it's minor

.filter(value => value !== 'world')
.map(value => {
return { entityType: value };
});
respond(res, entityTypes);
} catch (error) {
next(error);
}
};
4 changes: 4 additions & 0 deletions packages/central-server/src/apiV2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { GETDisasters } from './GETDisasters';
import { GETDataElements, EditDataElements, DeleteDataElements } from './dataElements';
import { GETDataGroups, EditDataGroups, DeleteDataGroups } from './dataGroups';
import { GETEntities } from './GETEntities';
import { getEntityTypes } from './getEntityTypes';
import { GETFeedItems } from './GETFeedItems';
import { GETGeographicalAreas } from './GETGeographicalAreas';
import { GETSurveyGroups } from './GETSurveyGroups';
Expand All @@ -42,6 +43,7 @@ import { DeleteSurveys, EditSurveys, GETSurveys } from './surveys';
import { GETProjects } from './GETProjects';
import { DeleteDashboardItem, EditDashboardItem, GETDashboardItems } from './dashboardItems';
import { CreateDashboard, DeleteDashboard, EditDashboard, GETDashboards } from './dashboards';
import { CreateProject } from './projects';
import {
DeleteDashboardRelation,
EditDashboardRelation,
Expand Down Expand Up @@ -176,6 +178,7 @@ apiV2.get('/mapOverlayGroupRelations/:recordId?', useRouteHandler(GETMapOverlayG
apiV2.get('/surveys/:recordId?', useRouteHandler(GETSurveys));
apiV2.get('/countries/:parentRecordId/surveys', useRouteHandler(GETSurveys));
apiV2.get('/countries/:parentRecordId/entities', useRouteHandler(GETEntities));
apiV2.get('/entityTypes', allowAnyone(getEntityTypes));
apiV2.get('/surveyGroups/:recordId?', useRouteHandler(GETSurveyGroups));
apiV2.get('/surveyResponses/:parentRecordId/answers', useRouteHandler(GETAnswers));
apiV2.get('/surveyResponses/:recordId?', useRouteHandler(GETSurveyResponses));
Expand Down Expand Up @@ -239,6 +242,7 @@ apiV2.post('/dashboardRelations', useRouteHandler(CreateDashboardRelation));
apiV2.post('/dashboardVisualisations', useRouteHandler(CreateDashboardVisualisation));
apiV2.post('/mapOverlayVisualisations', useRouteHandler(CreateMapOverlayVisualisation));
apiV2.post('/mapOverlayGroupRelations', useRouteHandler(CreateMapOverlayGroupRelation));
apiV2.post('/projects', useRouteHandler(CreateProject));
apiV2.post('/syncFromService', allowAnyone(manualKoBoSync));

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

import { CreateHandler } from '../CreateHandler';
import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions';

/**
* Handles POST endpoints:
* - /projects
*/

export class CreateProject extends CreateHandler {
async assertUserHasAccess() {
await this.assertPermissions(
assertAnyPermissions([assertBESAdminAccess], 'You need BES Admin to create new projects'),
);
}

async createRecord() {
const {
code,
name,
description,
sort_order,
image_url,
logo_url,
permission_groups,
countries,
entityTypes,
default_measure,
dashboard_group_name,
} = this.newRecordData;

await this.models.wrapInTransaction(async transactingModels => {
await this.createProjectEntity(transactingModels, code, name);
await this.createEntityHierarchy(transactingModels, code, entityTypes);
await this.createProjectEntityRelations(transactingModels, code, countries);
await this.createProjectDashboard(transactingModels, dashboard_group_name, code);

const { id: projectEntityId } = await transactingModels.entity.findOne({
'entity.code': code,
});
const { id: projectEntityHierarchyId } = await transactingModels.entityHierarchy.findOne({
'entity_hierarchy.name': code,
});

const { name: dashboardGroupName } = await transactingModels.dashboard.findOne({
'dashboard.root_entity_code': code,
});

return transactingModels.project.create({
code,
description,
sort_order,
image_url,
logo_url,
permission_groups,
default_measure,
dashboard_group_name: dashboardGroupName,
entity_id: projectEntityId,
entity_hierarchy_id: projectEntityHierarchyId,
});
});
}

async createProjectEntity(models, code, name) {
const worldCode = 'World';
const { id: worldId } = await models.entity.findOne({ 'entity.code': worldCode });

await models.entity.create({
Copy link
Contributor

@IgorNadj IgorNadj Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: .create() returns the record with an id, so you can return that instead of querying again to get it above on line 42. Same with the others

name,
code,
parent_id: worldId,
type: 'project',
});
}

async createProjectEntityRelations(models, code, countries) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest we rename code argument to projectCode? Took me a little bit of time to figure out what this was

const { id: projectEntityId } = await models.entity.findOne({
'entity.code': code,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: you don't need to prefix things here, you can just do { code: code }, same with others below

});
const { id: entityHierarchyId } = await models.entityHierarchy.findOne({
'entity_hierarchy.name': code,
});

for (const countryId of countries) {
const { code: countryCode } = await models.country.findOne({
'country.id': countryId,
});
const { id: entityId } = await models.entity.findOne({
'entity.code': countryCode,
'entity.type': 'country',
});
await models.entityRelation.create({
parent_id: projectEntityId,
child_id: entityId,
entity_hierarchy_id: entityHierarchyId,
});
}
}

async createProjectDashboard(models, dashboard_group_name, code) {
await models.dashboard.create({
code: `${code}_project`,
name: dashboard_group_name,
root_entity_code: code,
});
}

async createEntityHierarchy(models, code, entityTypes) {
await models.entityHierarchy.create({
name: code,
canonical_types: entityTypes ? `{${entityTypes.join(',')}}` : '{}',
});
}
}
6 changes: 6 additions & 0 deletions packages/central-server/src/apiV2/projects/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Tupaia
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

export { CreateProject } from './CreateProject';
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,55 @@ export const constructForSingle = (models, recordType) => {
code: [hasContent],
name: [hasContent],
};
case TYPES.PROJECT:
return {
code: [constructRecordNotExistsWithField(models.project, 'code')],
name: [isAString],
countries: [
async countries => {
const countryEntities = await models.country.find({
id: countries,
});
if (countryEntities.length !== countries.length) {
throw new Error('Some provided countries do not exist');
}
return true;
},
],
permission_groups: [
hasContent,
async permissionGroupNames => {
const permissionGroups = await models.permissionGroup.find({
name: permissionGroupNames,
});
if (permissionGroupNames.length !== permissionGroups.length) {
throw new Error('Some provided permission groups do not exist');
}
return true;
},
],
description: [isAString],
sort_order: [constructIsEmptyOr(isNumber)],
image_url: [isAString],
logo_url: [isAString],
entityTypes: [
async selectedEntityTypes => {
if (!selectedEntityTypes) {
return true;
}
const entityDataTypes = await models.entity.types;
const filteredEntityTypes = Object.values(entityDataTypes).filter(type =>
selectedEntityTypes.includes(type),
);
if (selectedEntityTypes.length !== filteredEntityTypes.length) {
throw new Error('Some provided entity types do not exist');
}
return true;
},
],
dashboard_group_name: [isAString],
default_measure: [constructRecordExistsWithField(models.mapOverlay, 'id')],
};
default:
throw new ValidationError(`${recordType} is not a valid POST endpoint`);
}
Expand Down
Loading