Skip to content

Commit

Permalink
Merge branch 'main' into dcloud/67-welcome-placard
Browse files Browse the repository at this point in the history
  • Loading branch information
dcloud authored Mar 1, 2021
2 parents 2480271 + 11f5258 commit ad65fdd
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 45 deletions.
99 changes: 97 additions & 2 deletions frontend/src/__tests__/permissions.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import isAdmin, { hasReadWrite } from '../permissions';
import { SCOPE_IDS } from '../Constants';
import isAdmin, { hasReadWrite, allRegionsUserHasPermissionTo, getRegionWithReadWrite } from '../permissions';

describe('permissions', () => {
describe('isAdmin', () => {
it('returns true if the user is an admin', () => {
const user = {
permissions: [
{
scopeId: 2,
scopeId: SCOPE_IDS.ADMIN,
},
],
};
Expand All @@ -21,6 +22,51 @@ describe('permissions', () => {
});
});

describe('allRegionsUserHasPermissionTo', () => {
it('returns an array with all the correct regions', () => {
const user = {
permissions: [
{
scopeId: SCOPE_IDS.ADMIN,
regionId: 14,
},
{
scopeId: SCOPE_IDS.SITE_ACCESS,
regionId: 14,
},
{
scopeId: SCOPE_IDS.SITE_ACCESS,
regionId: 1,
},
{
scopeId: SCOPE_IDS.APPROVE_ACTIVITY_REPORTS,
regionId: 1,
},
{
scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS,
regionId: 3,
},
{
scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS,
regionId: 4,
},
{
scopeId: SCOPE_IDS.APPROVE_ACTIVITY_REPORTS,
regionId: 4,
},
],
};
const regions = allRegionsUserHasPermissionTo(user);
expect(regions).toEqual(expect.arrayContaining([14, 3, 4]));
});

it('returns empty array when user has no permissions', () => {
const user = {};
const regions = allRegionsUserHasPermissionTo(user);
expect(regions).toEqual([]);
});
});

describe('hasReadWrite', () => {
it('returns true if the user has read/write to a region', () => {
const user = {
Expand All @@ -46,4 +92,53 @@ describe('permissions', () => {
expect(hasReadWrite(user)).toBeFalsy();
});
});

describe('getRegionWithReadWrite', () => {
it('returns region where user has permission', () => {
const user = {
permissions: [
{
regionId: 4,
scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS,
},
{
regionId: 1,
scopeId: SCOPE_IDS.ADMIN,
},
{
regionId: 2,
scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS,
},
],
};

const region = getRegionWithReadWrite(user);
expect(region).toBe(2);
});

it('returns no region', () => {
const user = {
permissions: [
{
regionId: 4,
scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS,
},
{
regionId: 1,
scopeId: SCOPE_IDS.ADMIN,
},
],
};

const region = getRegionWithReadWrite(user);
expect(region).toBe(-1);
});

it('returns region because user object has no permissions', () => {
const user = {};

const region = getRegionWithReadWrite(user);
expect(region).toBe(-1);
});
});
});
4 changes: 2 additions & 2 deletions frontend/src/fetchers/activityReports.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export const getReportAlerts = async () => {
return reports.json();
};

export const getRecipients = async () => {
const recipients = await get(join(activityReportUrl, 'activity-recipients'));
export const getRecipients = async (region) => {
const recipients = await get(join(activityReportUrl, 'activity-recipients', `?region=${region}`));
return recipients.json();
};

Expand Down
13 changes: 0 additions & 13 deletions frontend/src/hooks.js

This file was deleted.

1 change: 1 addition & 0 deletions frontend/src/pages/ActivityReport/Pages/activitySummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ const ActivitySummary = ({
name="duration"
type="number"
min={0}
step={0.5}
inputRef={
register({
required: 'Please enter the duration of the activity',
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/pages/ActivityReport/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ import userEvent from '@testing-library/user-event';

import { withText } from '../../../testHelpers';
import ActivityReport from '../index';
// import { getRegionWithReadWrite } from '../../../permissions';

jest.mock('../../../permissions', () => ({
getRegionWithReadWrite: jest.fn(() => 1),
}));

const formData = () => ({
regionId: 1,
deliveryMethod: 'in-person',
ttaType: ['training'],
duration: '1',
Expand Down Expand Up @@ -62,7 +68,7 @@ describe('ActivityReport', () => {
afterEach(() => fetchMock.restore());

beforeEach(() => {
fetchMock.get('/api/activity-reports/activity-recipients', recipients);
fetchMock.get('/api/activity-reports/activity-recipients?region=1', recipients);
fetchMock.get('/api/users/collaborators?region=1', []);
fetchMock.get('/api/activity-reports/approvers?region=1', []);
});
Expand All @@ -74,9 +80,18 @@ describe('ActivityReport', () => {
expect(alert).toHaveTextContent('Unable to load activity report');
});

it('handles when region is invalid', async () => {
fetchMock.get('/api/activity-reports/-1', () => { throw new Error('unable to download report'); });

renderActivityReport('-1');
const alert = await screen.findByTestId('alert');
expect(alert).toHaveTextContent('Unable to load activity report');
});

describe('last saved time', () => {
it('is shown if history.state.showLastUpdatedTime is true', async () => {
const data = formData();

fetchMock.get('/api/activity-reports/1', data);
renderActivityReport('1', 'activity-summary', true);
await screen.findByRole('group', { name: 'Who was the activity for?' }, { timeout: 4000 });
Expand All @@ -85,6 +100,7 @@ describe('ActivityReport', () => {

it('is not shown if history.state.showLastUpdatedTime is null', async () => {
const data = formData();

fetchMock.get('/api/activity-reports/1', data);
renderActivityReport('1', 'activity-summary');
await screen.findByRole('group', { name: 'Who was the activity for?' });
Expand Down
59 changes: 40 additions & 19 deletions frontend/src/pages/ActivityReport/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import { Helmet } from 'react-helmet';
import ReactRouterPropTypes from 'react-router-prop-types';
import { useHistory, Redirect } from 'react-router-dom';
import { Alert, Grid } from '@trussworks/react-uswds';
import useDeepCompareEffect from 'use-deep-compare-effect';
import moment from 'moment';

import pages from './Pages';
import Navigator from '../../components/Navigator';

import './index.css';
import { NOT_STARTED } from '../../components/Navigator/constants';
import { REPORT_STATUSES } from '../../Constants';
import { REPORT_STATUSES, DECIMAL_BASE } from '../../Constants';
import { getRegionWithReadWrite } from '../../permissions';
import {
submitReport,
saveReport,
Expand Down Expand Up @@ -55,12 +57,12 @@ const defaultValues = {
status: REPORT_STATUSES.DRAFT,
};

// FIXME: default region until we have a way of changing on the frontend
const region = 1;
const pagesByPos = _.keyBy(pages.filter((p) => !p.review), (page) => page.position);
const defaultPageState = _.mapValues(pagesByPos, () => NOT_STARTED);

function ActivityReport({ match, user, location }) {
function ActivityReport({
match, user, location, region,
}) {
const { params: { currentPage, activityReportId } } = match;
const history = useHistory();
const [error, updateError] = useState();
Expand All @@ -80,27 +82,30 @@ function ActivityReport({ match, user, location }) {
history.replace();
}, [activityReportId, history]);

useEffect(() => {
useDeepCompareEffect(() => {
const fetch = async () => {
let report;

try {
updateLoading(true);

const apiCalls = [
getRecipients(),
getCollaborators(region),
getApprovers(region),
];

if (activityReportId !== 'new') {
apiCalls.push(getReport(activityReportId));
report = await getReport(activityReportId);
} else {
apiCalls.push(
Promise.resolve({ ...defaultValues, pageState: defaultPageState, userId: user.id }),
);
report = {
...defaultValues,
pageState: defaultPageState,
userId: user.id,
regionId: region || getRegionWithReadWrite(user),
};
}

const [recipients, collaborators, approvers, report] = await Promise.all(apiCalls);
const apiCalls = [
getRecipients(report.regionId),
getCollaborators(report.regionId),
getApprovers(report.regionId),
];

const [recipients, collaborators, approvers] = await Promise.all(apiCalls);
reportId.current = activityReportId;

const isCollaborator = report.collaborators
Expand All @@ -120,12 +125,17 @@ function ActivityReport({ match, user, location }) {
updateError();
} catch (e) {
updateError('Unable to load activity report');
// If the error was caused by an invalid region, we need a way to communicate that to the
// component so we can redirect the user. We can do this by updating the form data
if (report && parseInt(report.regionId, DECIMAL_BASE) === -1) {
updateFormData({ regionId: report.regionId });
}
} finally {
updateLoading(false);
}
};
fetch();
}, [activityReportId, user.id, showLastUpdatedTime]);
}, [activityReportId, user, showLastUpdatedTime, region]);

if (loading) {
return (
Expand All @@ -135,6 +145,12 @@ function ActivityReport({ match, user, location }) {
);
}

// If no region was able to be found, we will re-reoute user to the main page
// FIXME: when re-routing user show a message explaining what happened
if (formData && parseInt(formData.regionId, DECIMAL_BASE) === -1) {
return <Redirect to="/" />;
}

if (error) {
return (
<Alert type="error">
Expand Down Expand Up @@ -165,7 +181,7 @@ function ActivityReport({ match, user, location }) {
if (canWrite) {
if (reportId.current === 'new') {
if (activityRecipientType && activityRecipients && activityRecipients.length > 0) {
const savedReport = await createReport({ ...data, regionId: region }, {});
const savedReport = await createReport({ ...data, regionId: formData.regionId }, {});
reportId.current = savedReport.id;
const current = pages.find((p) => p.path === currentPage);
updatePage(current.position);
Expand Down Expand Up @@ -226,11 +242,16 @@ function ActivityReport({ match, user, location }) {
ActivityReport.propTypes = {
match: ReactRouterPropTypes.match.isRequired,
location: ReactRouterPropTypes.location.isRequired,
region: PropTypes.number,
user: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
role: PropTypes.string,
}).isRequired,
};

ActivityReport.defaultProps = {
region: undefined,
};

export default ActivityReport;
46 changes: 46 additions & 0 deletions frontend/src/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,52 @@ const isAdmin = (user) => {
) !== undefined;
};

/**
* Return all regions that user has a minimum of read access to.
* All permissions that qualify this criteria are:
* Admin
* Read Activity Reports
* Read Write Activity Reports
* @param {*} - user object
* @returns {array} - An array of integers, where each integer signifies a region.
*/
// FIXME: Descide if we will keep this or remove
export const allRegionsUserHasPermissionTo = (user) => {
const permissions = _.get(user, 'permissions');

if (!permissions) return [];

const minPermissions = [
SCOPE_IDS.ADMIN,
SCOPE_IDS.READ_ACTIVITY_REPORTS,
SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS,
];

const regions = [];
permissions.forEach((perm) => {
if (minPermissions.includes(perm.scopeId)) {
regions.push(perm.regionId);
}
});

return _.uniq(regions);
};

/**
* Search the user's permissions for any region they have read/write permissions to.
* Return *first* region that matches this criteria. Otherwise return -1.
* @param {*} user - user object
* @returns {number} - region id if the user has read/write access for a region, -1 otherwise
*/

export const getRegionWithReadWrite = (user) => {
const { permissions } = user;
if (!permissions) return -1;

const perm = permissions.find((p) => p.scopeId === SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS);
return perm ? perm.regionId : -1;
};

/**
* Search the user's permissions for a read/write permisions for a region
* @param {*} user - user object
Expand Down
Loading

0 comments on commit ad65fdd

Please sign in to comment.