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

SW-321: Dataset Organisation & Team #53

Merged
merged 4 commits into from
Dec 10, 2024
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
54 changes: 52 additions & 2 deletions src/controllers/publish.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { snakeCase, sortBy } from 'lodash';
import { snakeCase, sortBy, uniqBy } from 'lodash';
import { FieldValidationError, matchedData } from 'express-validator';
import { nanoid } from 'nanoid';
import { v4 as uuid } from 'uuid';
Expand All @@ -21,9 +21,11 @@ import {
linkUrlValidator,
minuteValidator,
monthValidator,
organisationIdValidator,
qualityValidator,
roundingAppliedValidator,
roundingDescriptionValidator,
teamIdValidator,
titleValidator,
topicIdValidator,
yearValidator
Expand Down Expand Up @@ -52,6 +54,8 @@ import { TopicDTO } from '../dtos/topic';
import { DatasetTopicDTO } from '../dtos/dataset-topic';
import { nestTopics } from '../utils/nested-topics';
import { ApiException } from '../exceptions/api.exception';
import { OrganisationDTO } from '../dtos/organisation';
import { TeamDTO } from '../dtos/team';

export const start = (req: Request, res: Response, next: NextFunction) => {
res.render('publish/start');
Expand Down Expand Up @@ -702,7 +706,7 @@ export const providePublishDate = async (req: Request, res: Response, next: Next
throw new Error('form.validation');
}

await req.swapi.setPublishDate(dataset.id, revision.id, publishDate.toISOString());
await req.swapi.updatePublishDate(dataset.id, revision.id, publishDate.toISOString());
res.redirect(req.buildUrl(`/publish/${dataset.id}/tasklist`, req.language));
return;
} catch (err) {
Expand All @@ -714,3 +718,49 @@ export const providePublishDate = async (req: Request, res: Response, next: Next

res.render('publish/schedule', { values, errors, dateError, timeError });
};

export const provideOrganisation = async (req: Request, res: Response, next: NextFunction) => {
const { dataset } = res.locals;
let organisations: OrganisationDTO[] = [];
let teams: TeamDTO[] = [];
let errors: ViewError[] = [];
let values = { organisation: '', team: '' };

try {
organisations = await req.swapi.getAllOrganisations();
teams = await req.swapi.getAllTeams();

if (dataset.team_id) {
const datasetTeam = teams.find((team) => team.id === dataset.team_id)!;
values = { organisation: datasetTeam.organisation_id!, team: datasetTeam.id };
}

if (req.method === 'POST') {
values = req.body;
const validators = [organisationIdValidator(), teamIdValidator()];

errors = (await getErrors(validators, req)).map((error: FieldValidationError) => {
return { field: error.path, message: { key: `publish.organisation.form.${error.path}.error` } };
});

const selectedTeam = teams.find((team) => team.id === values.team);

if (!selectedTeam) {
errors.push({ field: 'team', message: { key: 'publish.organisation.form.team.error' } });
}

errors = uniqBy(errors, 'field');
if (errors.length > 0) throw errors;

await req.swapi.updateDatasetTeam(dataset.id, values.team);
res.redirect(req.buildUrl(`/publish/${dataset.id}/tasklist`, req.language));
return;
}
} catch (err) {
if (err instanceof ApiException) {
errors = [{ field: 'api', message: { key: 'publish.organisation.error.saving' } }];
}
}

res.render('publish/organisation', { values, organisations, teams, errors });
};
1 change: 1 addition & 0 deletions src/dtos/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface DatasetDTO {
datasetInfo?: DatasetInfoDTO[];
providers?: DatasetProviderDTO[];
topics?: DatasetTopicDTO[];
team_id?: string;
}
4 changes: 4 additions & 0 deletions src/dtos/organisation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class OrganisationDTO {
id: string;
name?: string;
}
10 changes: 10 additions & 0 deletions src/dtos/team.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { OrganisationDTO } from './organisation';

export class TeamDTO {
id: string;
prefix?: string;
name?: string;
email?: string;
organisation_id?: string;
organisation?: OrganisationDTO;
}
File renamed without changes.
19 changes: 19 additions & 0 deletions src/middleware/translations/en.json → src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,25 @@
"saving": "Could not save the publish date, please try later"
}
},
"organisation": {
"heading": "Publisher organisation and team",
"form": {
"organisation": {
"label": "Organisation",
"placeholder": "Select organisation",
"error": "Select an organisation"
},
"team": {
"label": "Team",
"note": "If you cannot find your team in the list, you can <a class=\"govuk-link\" href=\"{{request_team}}\">request a team be added</a>.",
"placeholder": "Select team",
"error": "Select a team"
}
},
"error": {
"saving": "Could not save the team, please try later"
}
},
"upload": {
"title": "Upload the data table",
"note": "The file should be in a CSV format"
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ i18next
cookieSecure: config.session.secure
},
backend: {
loadPath: `${__dirname}/translations/{{lng}}.json`
loadPath: `${__dirname}/../i18n/{{lng}}.json`
},
fallbackLng: config.language.fallback,
preload: TRANSLATIONS,
Expand Down
6 changes: 5 additions & 1 deletion src/routes/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
provideUpdateFrequency,
provideDesignation,
provideTopics,
providePublishDate
providePublishDate,
provideOrganisation
} from '../controllers/publish';

export const publish = Router();
Expand Down Expand Up @@ -76,3 +77,6 @@ publish.post('/:datasetId/topics', fetchDataset, upload.none(), provideTopics);

publish.get('/:datasetId/schedule', fetchDataset, providePublishDate);
publish.post('/:datasetId/schedule', fetchDataset, upload.none(), providePublishDate);

publish.get('/:datasetId/organisation', fetchDataset, provideOrganisation);
publish.post('/:datasetId/organisation', fetchDataset, upload.none(), provideOrganisation);
27 changes: 26 additions & 1 deletion src/services/stats-wales-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { DatasetProviderDTO } from '../dtos/dataset-provider';
import { ProviderDTO } from '../dtos/provider';
import { ProviderSourceDTO } from '../dtos/provider-source';
import { TopicDTO } from '../dtos/topic';
import { OrganisationDTO } from '../dtos/organisation';
import { TeamDTO } from '../dtos/team';

const config = appConfig();

Expand Down Expand Up @@ -288,11 +290,34 @@ export class StatsWalesApi {
);
}

public async setPublishDate(datasetId: string, revisionId: string, publishDate: string): Promise<DatasetDTO> {
public async updatePublishDate(datasetId: string, revisionId: string, publishDate: string): Promise<DatasetDTO> {
return this.fetch({
url: `dataset/${datasetId}/revision/by-id/${revisionId}/publish-at`,
method: HttpMethod.Patch,
json: { publish_at: publishDate }
}).then((response) => response.json() as unknown as DatasetDTO);
}

public async getAllOrganisations(): Promise<OrganisationDTO[]> {
logger.debug('Fetching organisations...');
return this.fetch({ url: 'organisation' }).then((response) => response.json() as unknown as OrganisationDTO[]);
}

public async getAllTeams(): Promise<TeamDTO[]> {
logger.debug('Fetching teams...');
return this.fetch({ url: 'team' }).then((response) => response.json() as unknown as TeamDTO[]);
}

public async getTeam(teamId: string): Promise<TeamDTO> {
logger.debug('Fetching team...');
return this.fetch({ url: `team/${teamId}` }).then((response) => response.json() as unknown as TeamDTO);
}

public async updateDatasetTeam(datasetId: string, teamId: string): Promise<DatasetDTO> {
logger.debug('Updating dataset team...');
const data = { team_id: teamId };
return this.fetch({ url: `dataset/${datasetId}/team`, method: HttpMethod.Patch, json: data }).then(
(response) => response.json() as unknown as DatasetDTO
);
}
}
3 changes: 3 additions & 0 deletions src/validators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ export const monthValidator = () => body('month').isInt({ min: 1, max: 12, allow
export const yearValidator = () => body('year').isInt({ min: new Date().getFullYear(), allow_leading_zeroes: true });
export const hourValidator = () => body('hour').isInt({ min: 0, max: 23, allow_leading_zeroes: true });
export const minuteValidator = () => body('minute').isInt({ min: 0, max: 59, allow_leading_zeroes: true });

export const organisationIdValidator = () => body('organisation').trim().notEmpty().isUUID(4);
export const teamIdValidator = () => body('team').trim().notEmpty().isUUID(4);
82 changes: 82 additions & 0 deletions src/views/publish/organisation.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<%- include("../partials/header"); %>
<div class="govuk-width-container app-width-container">
<main class="govuk-main-wrapper" id="main-content" role="main">

<div class="top-links">
<div class="govuk-width-container">
<a href="javascript:history.back()" class="govuk-back-link"><%= t('buttons.back') %></a>
<a href="<%= buildUrl(`/publish/${locals.datasetId}/tasklist`, i18n.language) %>" class="govuk-link return-link"><%= t('publish.header.overview') %></a>
</div>
</div>

<div class="govuk-width-container">
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl"><%= t('publish.organisation.heading') %></h1>

<%- include("../partials/error-handler"); %>

<form enctype="multipart/form-data" method="post">
<div class="govuk-form-group <%= locals.errors?.find(e => e.field === 'organisation') ? 'govuk-input--error' : '' %>">
<h2 class="govuk-heading-s"><%= t('publish.organisation.form.organisation.label') %></h2>
<% if (locals.errors?.find(e => e.field === 'organisation')) { %>
<p id="publication-time-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> <%= t('publish.organisation.form.organisation.error') %>
</p>
<% } %>
<select id="organisation" name="organisation" class="govuk-select" aria-describedby="source-hint" data-child="team">
<option value="" selected disabled><%= t('publish.organisation.form.organisation.placeholder') %></option>
<% for (let organisation of locals.organisations) { %>
<option value="<%= organisation.id %>" <%= locals.values?.organisation === organisation.id ? 'selected' : '' %>><%= organisation.name %></option>
<% } %>
</select>
</div>
<div class="govuk-form-group <%= locals.errors?.find(e => e.field === 'team') ? 'govuk-input--error' : '' %>">
<h2 class="govuk-heading-s"><%= t('publish.organisation.form.team.label') %></h2>
<p class="govuk-body"><%- t('publish.organisation.form.team.note') %></p>
<% if (locals.errors?.find(e => e.field === 'team')) { %>
<p id="publication-time-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> <%= t('publish.organisation.form.team.error') %>
</p>
<% } %>
<select id="team" name="team" class="govuk-select" aria-describedby="source-hint">
<option value="" selected disabled><%= t('publish.organisation.form.team.placeholder') %></option>
<% for (let team of locals.teams) { %>
<option value="<%= team.id %>" <%= locals.values?.team === team.id ? 'selected' : '' %> data-org="<%= team.organisation_id %>"><%= team.name %></option>
<% } %>
</select>
</div>
<button type="submit" class="govuk-button" data-module="govuk-button"><%= t('buttons.continue') %></button>
<a class="govuk-button govuk-button--secondary" href="javascript:history.back()"><%= t('buttons.cancel')%></a>
</form>
</div>
</div>
</div>
</main>
</div>

<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
const organisation = document.getElementById('organisation');
const team = document.getElementById('team');

organisation.addEventListener('change', function () {
const selectedOrgId = organisation.options[organisation.selectedIndex].value;
const selectedTeamId = '<%= locals?.values?.team %>';
const teamOptions = team.querySelectorAll('option');
team.selectedIndex = 0;

teamOptions.forEach(function (option) {
option.style.display = option.dataset.org === selectedOrgId || option.value === '' ? 'block' : 'none';

if (selectedTeamId && option.value === selectedTeamId && option.style.display == 'block') {
option.selected = true;
}
});
});

organisation.dispatchEvent(new Event('change'));
});
</script>

<%- include("../partials/footer"); %>
2 changes: 1 addition & 1 deletion src/views/publish/tasklist.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
<ul class="govuk-task-list">
<li class="govuk-task-list__item govuk-task-list__item--with-link">
<div class="govuk-task-list__name-and-hint">
<a class="govuk-link govuk-task-list__link" href="<%= buildUrl(`/publish/${locals.datasetId}/contacts`, i18n.language) %>" aria-describedby="prepare-application-5-status">
<a class="govuk-link govuk-task-list__link" href="<%= buildUrl(`/publish/${locals.datasetId}/organisation`, i18n.language) %>" aria-describedby="prepare-application-5-status">
<%= t('publish.tasklist.publishing.organisation') %>
</a>
</div>
Expand Down
2 changes: 1 addition & 1 deletion tools/copy-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ import * as shell from 'shelljs';
shell.cp('-R', 'src/views', 'dist/');
shell.cp('-R', 'src/public/assets', 'dist/');
shell.cp('-R', 'src/public/css', 'dist/');
shell.cp('-R', 'src/middleware/translations', 'dist/middleware/translations');
shell.cp('-R', 'src/i18n', 'dist/i18n/');
Loading