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

RN-423: Add 'Sync Groups' to the Admin Panel (kobo sync link part 1) #4088

Merged
merged 15 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions packages/admin-panel/src/editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
EDITOR_DISMISS,
EDITOR_ERROR,
EDITOR_FIELD_EDIT,
EDITOR_OPEN_CREATOR,
EDITOR_OPEN,
} from './constants';
import { convertSearchTermToFilter, makeSubstitutionsInString } from '../utilities';

Expand Down Expand Up @@ -68,7 +68,7 @@ export const openBulkEditModal = (
});

dispatch({
type: EDITOR_OPEN_CREATOR,
type: EDITOR_OPEN,
fields,
recordData: {},
endpoint: bulkUpdateEndpoint,
Expand Down Expand Up @@ -126,7 +126,7 @@ export const openEditModal = ({ editEndpoint, fields }, recordId) => async (
});

dispatch({
type: EDITOR_OPEN_CREATOR,
type: EDITOR_OPEN,
fields,
recordData: {},
endpoint: editEndpoint,
Expand Down
2 changes: 1 addition & 1 deletion packages/admin-panel/src/editor/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const EDITOR_DATA_EDIT_SUCCESS = 'EDITOR_DATA_EDIT_SUCCESS';
export const EDITOR_DISMISS = 'EDITOR_DISMISS';
export const EDITOR_ERROR = 'EDITOR_ERROR';
export const EDITOR_FIELD_EDIT = 'EDITOR_FIELD_EDIT';
export const EDITOR_OPEN_CREATOR = 'EDITOR_OPEN_CREATOR';
export const EDITOR_OPEN = 'EDITOR_OPEN';

export const DATA_CHANGE_ACTIONS = {
start: EDITOR_DATA_EDIT_BEGIN,
Expand Down
4 changes: 2 additions & 2 deletions packages/admin-panel/src/editor/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
EDITOR_DISMISS,
EDITOR_ERROR,
EDITOR_FIELD_EDIT,
EDITOR_OPEN_CREATOR,
EDITOR_OPEN,
} from './constants';

const defaultState = {
Expand Down Expand Up @@ -52,7 +52,7 @@ const stateChanges = {
}
return defaultState; // If no error, dismiss the whole modal and clear its state
},
[EDITOR_OPEN_CREATOR]: payload => payload,
[EDITOR_OPEN]: payload => payload,
[EDITOR_FIELD_EDIT]: ({ fieldKey, newValue }, { editedFields }) => ({
editedFields: {
...editedFields,
Expand Down
11 changes: 0 additions & 11 deletions packages/admin-panel/src/pages/resources/IndicatorsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,6 @@ const FIELDS = [
editConfig: {
type: 'jsonEditor',
default: '{ "formula": "", "aggregation": { "" : "" } }',
getJsonFieldSchema: () => [
{
label: 'Formula',
fieldName: 'formula',
},
{
label: 'Aggregation',
fieldName: 'aggregation',
type: 'object',
},
],
},
},
];
Expand Down
81 changes: 81 additions & 0 deletions packages/admin-panel/src/pages/resources/SyncGroupsPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Tupaia MediTrak
* Copyright (c) 2017 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import PropTypes from 'prop-types';
import { ResourcePage } from './ResourcePage';

const SERVICE_TYPES = [{ label: 'Kobo', value: 'kobo' }];

const FIELDS = [
{
Header: 'Survey Code',
source: 'data_group_code',
},
{
Header: 'Service Type',
source: 'service_type',
editConfig: {
options: SERVICE_TYPES,
},
},
{
Header: 'Config',
source: 'config',
type: 'jsonTooltip',
editConfig: {
type: 'jsonEditor',
default: '{}',
},
},
];

const COLUMNS = [
...FIELDS,
{
Header: 'Edit',
type: 'edit',
source: 'id',
actionConfig: {
editEndpoint: 'dataServiceSyncGroups',
fields: [...FIELDS],
},
},
{
Header: 'Delete',
source: 'id',
type: 'delete',
actionConfig: {
endpoint: 'dataServiceSyncGroups',
},
},
];

const EDIT_CONFIG = {
title: 'Edit Sync Group',
};

const CREATE_CONFIG = {
title: 'Add Sync Group',
actionConfig: {
editEndpoint: 'dataServiceSyncGroups',
fields: FIELDS,
},
};

export const SyncGroupsPage = ({ getHeaderEl }) => (
<ResourcePage
title="Sync Groups"
endpoint="dataServiceSyncGroups"
columns={COLUMNS}
editConfig={EDIT_CONFIG}
createConfig={CREATE_CONFIG}
getHeaderEl={getHeaderEl}
/>
);

SyncGroupsPage.propTypes = {
getHeaderEl: PropTypes.func.isRequired,
};
1 change: 1 addition & 0 deletions packages/admin-panel/src/pages/resources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export { DashboardItemsPage } from './DashboardItemsPage';
export { DashboardRelationsPage } from './DashboardRelationsPage';
export { LegacyReportsPage } from './LegacyReportsPage';
export { ProjectsPage } from './ProjectsPage';
export { SyncGroupsPage } from './SyncGroupsPage';
6 changes: 6 additions & 0 deletions packages/admin-panel/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DataElementsPage,
DataGroupsPage,
ProjectsPage,
SyncGroupsPage,
} from './pages/resources';

export const ROUTES = [
Expand Down Expand Up @@ -68,6 +69,11 @@ export const ROUTES = [
to: '/survey-responses',
component: SurveyResponsesPage,
},
{
label: 'Sync Groups',
to: '/sync-groups',
component: SyncGroupsPage,
},
],
},
{
Expand Down
5 changes: 5 additions & 0 deletions packages/central-server/src/apiV2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import {
EditMapOverlayVisualisation,
GETMapOverlayVisualisations,
} from './mapOverlayVisualisations';
import { GETSyncGroups, EditSyncGroups, CreateSyncGroups, DeleteSyncGroups } from './syncGroups';

// quick and dirty permission wrapper for open endpoints
const allowAnyone = routeHandler => (req, res, next) => {
Expand Down Expand Up @@ -212,6 +213,7 @@ apiV2.get('/facilities/:recordId?', useRouteHandler(GETClinics));
apiV2.get('/geographicalAreas/:recordId?', useRouteHandler(GETGeographicalAreas));
apiV2.get('/reports/:recordId?', useRouteHandler(GETReports));
apiV2.get('/dhisInstances/:recordId?', useRouteHandler(BESAdminGETHandler));
apiV2.get('/dataServiceSyncGroups/:recordId?', useRouteHandler(GETSyncGroups));

/**
* POST routes
Expand Down Expand Up @@ -243,6 +245,7 @@ apiV2.post('/dashboardVisualisations', useRouteHandler(CreateDashboardVisualisat
apiV2.post('/mapOverlayVisualisations', useRouteHandler(CreateMapOverlayVisualisation));
apiV2.post('/mapOverlayGroupRelations', useRouteHandler(CreateMapOverlayGroupRelation));
apiV2.post('/syncFromService', allowAnyone(manualKoBoSync));
apiV2.post('/dataServiceSyncGroups', useRouteHandler(CreateSyncGroups));

/**
* PUT routes
Expand Down Expand Up @@ -274,6 +277,7 @@ apiV2.put('/mapOverlayGroupRelations/:recordId', useRouteHandler(EditMapOverlayG
apiV2.put('/indicators/:recordId', useRouteHandler(BESAdminEditHandler));
apiV2.put('/projects/:recordId', useRouteHandler(BESAdminEditHandler));
apiV2.put('/me', catchAsyncErrors(editUser));
apiV2.put('/dataServiceSyncGroups/:recordId', useRouteHandler(EditSyncGroups));

/**
* DELETE routes
Expand Down Expand Up @@ -302,6 +306,7 @@ apiV2.delete(
useRouteHandler(DeleteMapOverlayGroupRelations),
);
apiV2.delete('/indicators/:recordId', useRouteHandler(BESAdminDeleteHandler));
apiV2.delete('/dataServiceSyncGroups/:recordId', useRouteHandler(DeleteSyncGroups));

apiV2.use(handleError); // error handler must come last

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

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

export class CreateSyncGroups extends CreateHandler {
async assertUserHasAccess() {
await this.assertPermissions(
assertAnyPermissions(
[assertBESAdminAccess, assertAdminPanelAccess],
'You need either BES Admin or Tupaia Admin Panel access to create a Sync Group',
),
);
}

async createRecord() {
return this.insertRecord();
}
}
17 changes: 17 additions & 0 deletions packages/central-server/src/apiV2/syncGroups/DeleteSyncGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { DeleteHandler } from '../DeleteHandler';
import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions';
import { assertSyncGroupEditPermissions } from './assertSyncGroupPermissions';

export class DeleteSyncGroups extends DeleteHandler {
async assertUserHasAccess() {
const syncGroupChecker = accessPolicy =>
assertSyncGroupEditPermissions(accessPolicy, this.models, this.recordId);

await this.assertPermissions(assertAnyPermissions([assertBESAdminAccess, syncGroupChecker]));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We may need to consider how to change this permission check... I suspect there will be some lesmis users who want to configure the sync group but don't have BES Admin...

}
}
21 changes: 21 additions & 0 deletions packages/central-server/src/apiV2/syncGroups/EditSyncGroups.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 { EditHandler } from '../EditHandler';
import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions';
import { assertSyncGroupEditPermissions } from './assertSyncGroupPermissions';

export class EditSyncGroups extends EditHandler {
async assertUserHasAccess() {
const syncGroupChecker = accessPolicy =>
assertSyncGroupEditPermissions(accessPolicy, this.models, this.recordId);

await this.assertPermissions(assertAnyPermissions([assertBESAdminAccess, syncGroupChecker]));
}

async editRecord() {
await this.updateRecord();
}
}
20 changes: 20 additions & 0 deletions packages/central-server/src/apiV2/syncGroups/GETSyncGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { GETHandler } from '../GETHandler';
import { assertAdminPanelAccess } from '../../permissions';
import { createSyncGroupDBFilter } from './assertSyncGroupPermissions';

export class GETSyncGroups extends GETHandler {
permissionsFilteredInternally = true;

async assertUserHasAccess() {
await this.assertPermissions(assertAdminPanelAccess);
}

async getPermissionsFilter(criteria, options) {
return createSyncGroupDBFilter(this.accessPolicy, this.models, criteria, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { QUERY_CONJUNCTIONS, TYPES } from '@tupaia/database';
import { assertSurveyEditPermissions } from '../surveys/assertSurveyPermissions';
import { hasBESAdminAccess } from '../../permissions';
import { fetchCountryIdsByPermissionGroupId, mergeMultiJoin } from '../utilities';

const { RAW } = QUERY_CONJUNCTIONS;

export const assertSyncGroupEditPermissions = async (accessPolicy, models, syncGroupId) => {
const syncGroup = await models.dataServiceSyncGroup.findById(syncGroupId);
if (!syncGroup) {
throw new Error(`No Sync Group exists with id ${syncGroupId}`);
}

const dataGroup = await models.dataGroup.findOne({ code: syncGroup.data_group_code });
if (!dataGroup) {
throw new Error(`Sync Group is not linked to an existing Data Group`);
}

const survey = await models.survey.findOne({ data_group_id: dataGroup.id });
if (!survey) {
throw new Error(`No Survey found for Data Group used by Sync Group`);
}

return assertSurveyEditPermissions(accessPolicy, models, survey.id);
};

export const createSyncGroupDBFilter = async (accessPolicy, models, criteria, options) => {
const dbConditions = { ...criteria };
const dbOptions = { ...options };

if (hasBESAdminAccess(accessPolicy)) {
return { dbConditions, dbOptions };
}

const countryIdsByPermissionGroupId = await fetchCountryIdsByPermissionGroupId(
accessPolicy,
models,
);

dbOptions.multiJoin = mergeMultiJoin(
[
{
joinWith: TYPES.DATA_GROUP,
joinCondition: [
`${TYPES.DATA_GROUP}.code`,
`${TYPES.DATA_SERVICE_SYNC_GROUP}.data_group_code`,
],
},
{
joinWith: TYPES.SURVEY,
joinCondition: [`${TYPES.SURVEY}.data_group_id`, `${TYPES.DATA_GROUP}.id`],
},
],
dbOptions.multiJoin,
);

dbConditions[RAW] = {
sql: `
(
survey.country_ids
&&
ARRAY(
SELECT TRIM('"' FROM JSON_ARRAY_ELEMENTS(?::JSON->survey.permission_group_id)::TEXT)
)
)`,
parameters: JSON.stringify(countryIdsByPermissionGroupId),
};

return { dbConditions, dbOptions };
};
9 changes: 9 additions & 0 deletions packages/central-server/src/apiV2/syncGroups/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

export { DeleteSyncGroups } from './DeleteSyncGroups';
export { EditSyncGroups } from './EditSyncGroups';
export { GETSyncGroups } from './GETSyncGroups';
export { CreateSyncGroups } from './CreateSyncGroups';
Loading