Implement schedule WYSIWYG and UTC dates
This commit is contained in:
@@ -274,6 +274,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
const data = await common.fetchTemplatesData(pool);
|
||||
const rssData = await common.fetchRssFeedsData(pool);
|
||||
const apiData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] };
|
||||
const scheduleData = typeof common.fetchSchedulesData === 'function' ? await common.fetchSchedulesData(pool) : { scheduleGroups: [] };
|
||||
const apiSources = Array.isArray(apiData.apiSources) ? apiData.apiSources.map(function (source) {
|
||||
return Object.assign({}, source, {
|
||||
responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null
|
||||
@@ -287,7 +288,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item;
|
||||
}) });
|
||||
}));
|
||||
Object.assign(data, rssData, apiData, { rssFeeds: rssFeeds, apiSources: apiSources, fontLibrary: loadFontLibrary(deps.uploadDir) });
|
||||
Object.assign(data, rssData, apiData, scheduleData, { rssFeeds: rssFeeds, apiSources: apiSources, scheduleGroups: scheduleData.scheduleGroups || [], fontLibrary: loadFontLibrary(deps.uploadDir) });
|
||||
res.send(pages.renderSlideFormPage(data, 'create', null, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -304,6 +305,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
const data = await common.fetchTemplatesData(pool);
|
||||
const rssData = await common.fetchRssFeedsData(pool);
|
||||
const apiData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] };
|
||||
const scheduleData = typeof common.fetchSchedulesData === 'function' ? await common.fetchSchedulesData(pool) : { scheduleGroups: [] };
|
||||
const apiSources = Array.isArray(apiData.apiSources) ? apiData.apiSources.map(function (source) {
|
||||
return Object.assign({}, source, {
|
||||
responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null
|
||||
@@ -317,7 +319,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item;
|
||||
}) });
|
||||
}));
|
||||
Object.assign(data, rssData, apiData, { rssFeeds: rssFeeds, apiSources: apiSources, fontLibrary: loadFontLibrary(deps.uploadDir) });
|
||||
Object.assign(data, rssData, apiData, scheduleData, { rssFeeds: rssFeeds, apiSources: apiSources, scheduleGroups: scheduleData.scheduleGroups || [], fontLibrary: loadFontLibrary(deps.uploadDir) });
|
||||
res.send(pages.renderSlideFormPage(data, 'edit', slide, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
const getAuditUserId = deps.getAuditUserId;
|
||||
const redirectAfterSave = deps.redirectAfterSave;
|
||||
const requirePermission = deps.requirePermission;
|
||||
const parseDateTimeLocal = deps.parseDateTimeLocal;
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { createDataSourceTaskService } = require('#src/web/lib/background-tasks/tasks-scheduled/data-source-refresh');
|
||||
|
||||
@@ -73,6 +74,7 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
||||
const apiSourceIds = new Set();
|
||||
const rssFeedIds = new Set();
|
||||
const scheduleGroupIds = new Set();
|
||||
|
||||
for (let index = 0; index < slides.length; index += 1) {
|
||||
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(slides[index].content_json) : null;
|
||||
@@ -104,6 +106,13 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(value, 'schedule_group_id')) {
|
||||
const scheduleGroupId = Number(value.schedule_group_id);
|
||||
if (Number.isFinite(scheduleGroupId)) {
|
||||
scheduleGroupIds.add(scheduleGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
walk(value[key]);
|
||||
});
|
||||
@@ -112,10 +121,28 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
|
||||
return {
|
||||
apiSourceIds: apiSourceIds,
|
||||
rssFeedIds: rssFeedIds
|
||||
rssFeedIds: rssFeedIds,
|
||||
scheduleGroupIds: scheduleGroupIds
|
||||
};
|
||||
}
|
||||
|
||||
function readScheduleArrayField(body, key) {
|
||||
if (!body || !Object.prototype.hasOwnProperty.call(body, key)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const value = body[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
|
||||
if (!pool || !common || !pages || typeof getAuditUserId !== 'function' || typeof redirectAfterSave !== 'function' || typeof requirePermission !== 'function' || !backgroundTaskQueue) {
|
||||
throw new Error('registerDataSourceRoutes requires the data source route dependencies.');
|
||||
}
|
||||
@@ -150,7 +177,10 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
if (hasAnyPermission(req.currentUser, ['rss-feeds.read', 'api-sources.read'])) {
|
||||
if (hasAnyPermission(req.currentUser, ['schedules.read', 'rss-feeds.read', 'api-sources.read'])) {
|
||||
if (hasAnyPermission(req.currentUser, ['schedules.read'])) {
|
||||
return res.redirect('/data-sources/schedules');
|
||||
}
|
||||
if (hasAnyPermission(req.currentUser, ['rss-feeds.read'])) {
|
||||
return res.redirect('/data-sources/rss-feeds');
|
||||
}
|
||||
@@ -560,4 +590,263 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/data-sources/schedules', requirePermission('schedules.read'), async function (req, res, next) {
|
||||
try {
|
||||
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
||||
const search = common.getSearchQuery(req);
|
||||
const sort = common.getSortQuery(req);
|
||||
const direction = common.getSortDirectionQuery(req);
|
||||
const data = await common.fetchScheduleGroupsPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
|
||||
const usageMaps = await getDataSourceUsageMaps();
|
||||
const scheduleGroups = (data.scheduleGroups || []).map(function (scheduleGroup) {
|
||||
return Object.assign({}, scheduleGroup, {
|
||||
inUse: usageMaps.scheduleGroupIds.has(Number(scheduleGroup.id))
|
||||
});
|
||||
});
|
||||
|
||||
res.send(pages.renderScheduleGroupsPage({
|
||||
scheduleGroups: scheduleGroups,
|
||||
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'Schedules', 'Schedule groups')
|
||||
}, req.query.message ? String(req.query.message) : '', req.currentUser, formatDashboardDate));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/data-sources/schedules/new', requirePermission('schedules.create'), function (req, res) {
|
||||
res.send(pages.renderScheduleGroupFormPage(null, [], 'create', req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
});
|
||||
|
||||
function readScheduleArrayFieldValue(body, keys) {
|
||||
const searchKeys = Array.isArray(keys) ? keys : [keys];
|
||||
for (let index = 0; index < searchKeys.length; index += 1) {
|
||||
const key = searchKeys[index];
|
||||
const value = body && Object.prototype.hasOwnProperty.call(body, key) ? body[key] : undefined;
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
return [value];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function buildScheduleEntryRows(req) {
|
||||
const body = req.body || {};
|
||||
const ids = readScheduleArrayFieldValue(body, ['entry_id[]', 'entry_id']);
|
||||
const titles = readScheduleArrayFieldValue(body, ['entry_title[]', 'entry_title']);
|
||||
const descriptions = readScheduleArrayFieldValue(body, ['entry_short_description[]', 'entry_short_description']);
|
||||
const starts = readScheduleArrayFieldValue(body, ['entry_start_datetime[]', 'entry_start_datetime']);
|
||||
const ends = readScheduleArrayFieldValue(body, ['entry_end_datetime[]', 'entry_end_datetime']);
|
||||
|
||||
const lengths = [ids.length, titles.length, descriptions.length, starts.length, ends.length].filter(Boolean);
|
||||
if (lengths.length && lengths.some(function (value) { return value !== lengths[0]; })) {
|
||||
const error = new Error('Schedule entry data is invalid.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
for (let index = 0; index < titles.length; index += 1) {
|
||||
const idValue = Number(ids[index]);
|
||||
const title = String(titles[index] || '').trim();
|
||||
const shortDescription = String(descriptions[index] || '').trim();
|
||||
const startDatetime = parseDateTimeLocal(starts[index]);
|
||||
const endDatetime = parseDateTimeLocal(ends[index]);
|
||||
const hasContent = title || shortDescription || String(starts[index] || '').trim() || String(ends[index] || '').trim() || Number.isFinite(idValue);
|
||||
|
||||
if (!hasContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
const error = new Error('Each schedule entry requires a title.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!startDatetime) {
|
||||
const error = new Error('Each schedule entry requires a start datetime.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (endDatetime && endDatetime < startDatetime) {
|
||||
const error = new Error('Schedule entry end datetime must be after the start datetime.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: Number.isFinite(idValue) && idValue > 0 ? idValue : null,
|
||||
title: title,
|
||||
shortDescription: shortDescription,
|
||||
startDatetime: startDatetime,
|
||||
endDatetime: endDatetime
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
app.post('/data-sources/schedules', requirePermission('schedules.create'), async function (req, res, next) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const payload = common.buildScheduleGroupPayload(req, null);
|
||||
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name)) {
|
||||
return res.redirect('/data-sources/schedules/new?message=' + encodeURIComponent('A schedule group with that name already exists.'));
|
||||
}
|
||||
|
||||
const entryRows = buildScheduleEntryRows(req);
|
||||
const actorId = getAuditUserId(req);
|
||||
await connection.beginTransaction();
|
||||
const [result] = await connection.query(
|
||||
'INSERT INTO i_schedule_groups (name, short_description, created_by, modified_by) VALUES (?, ?, ?, ?)',
|
||||
[payload.name, payload.shortDescription || null, actorId, actorId]
|
||||
);
|
||||
|
||||
if (entryRows.length) {
|
||||
const insertRows = entryRows.map(function (entry) {
|
||||
return [
|
||||
result.insertId,
|
||||
entry.title,
|
||||
entry.shortDescription || null,
|
||||
entry.startDatetime,
|
||||
entry.endDatetime || null,
|
||||
actorId,
|
||||
actorId
|
||||
];
|
||||
});
|
||||
|
||||
await connection.query(
|
||||
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
[insertRows]
|
||||
);
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
redirectAfterSave(req, res, '/data-sources/schedules/' + result.insertId + '/edit', {
|
||||
closeUrl: '/data-sources/schedules',
|
||||
newUrl: '/data-sources/schedules/new',
|
||||
message: 'Schedule group created.'
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await connection.rollback();
|
||||
} catch (_rollbackError) {
|
||||
// Ignore rollback failures and surface the original error.
|
||||
}
|
||||
next(error);
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/data-sources/schedules/:id/edit', requirePermission('schedules.update'), async function (req, res, next) {
|
||||
try {
|
||||
const scheduleGroup = await common.fetchScheduleGroupById(pool, Number(req.params.id));
|
||||
if (!scheduleGroup) {
|
||||
return res.status(404).send('Schedule group not found');
|
||||
}
|
||||
|
||||
const inUse = await getDataSourceInUse('schedule_group_id', scheduleGroup.id);
|
||||
const scheduleEntries = typeof common.fetchScheduleEntriesByGroupId === 'function'
|
||||
? await common.fetchScheduleEntriesByGroupId(pool, scheduleGroup.id)
|
||||
: [];
|
||||
|
||||
res.send(pages.renderScheduleGroupFormPage(Object.assign({}, scheduleGroup, {
|
||||
shortDescription: scheduleGroup.short_description || '',
|
||||
inUse: inUse
|
||||
}), scheduleEntries, 'edit', req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/data-sources/schedules/:id', requirePermission('schedules.update'), async function (req, res, next) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
const scheduleGroup = await common.fetchScheduleGroupById(pool, Number(req.params.id));
|
||||
if (!scheduleGroup) {
|
||||
return res.status(404).send('Schedule group not found');
|
||||
}
|
||||
|
||||
const payload = common.buildScheduleGroupPayload(req, scheduleGroup);
|
||||
if (await common.fetchDuplicateName(pool, 'i_schedule_groups', payload.name, scheduleGroup.id)) {
|
||||
return res.redirect('/data-sources/schedules/' + scheduleGroup.id + '/edit?message=' + encodeURIComponent('A schedule group with that name already exists.'));
|
||||
}
|
||||
|
||||
const entryRows = buildScheduleEntryRows(req);
|
||||
const actorId = getAuditUserId(req);
|
||||
await connection.beginTransaction();
|
||||
await connection.query(
|
||||
'UPDATE i_schedule_groups SET name = ?, short_description = ?, modified_by = ? WHERE id = ?',
|
||||
[payload.name, payload.shortDescription || null, actorId, scheduleGroup.id]
|
||||
);
|
||||
await connection.query('DELETE FROM i_schedule_entries WHERE schedule_group_id = ?', [scheduleGroup.id]);
|
||||
|
||||
if (entryRows.length) {
|
||||
const insertRows = entryRows.map(function (entry) {
|
||||
return [
|
||||
scheduleGroup.id,
|
||||
entry.title,
|
||||
entry.shortDescription || null,
|
||||
entry.startDatetime,
|
||||
entry.endDatetime || null,
|
||||
actorId,
|
||||
actorId
|
||||
];
|
||||
});
|
||||
|
||||
await connection.query(
|
||||
'INSERT INTO i_schedule_entries (schedule_group_id, title, short_description, start_datetime, end_datetime, created_by, modified_by) VALUES ?',
|
||||
[insertRows]
|
||||
);
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
const message = 'Schedule group updated.';
|
||||
res.redirect('/data-sources/schedules/' + scheduleGroup.id + '/edit?message=' + encodeURIComponent(message));
|
||||
} catch (error) {
|
||||
try {
|
||||
await connection.rollback();
|
||||
} catch (_rollbackError) {
|
||||
// Ignore rollback failures and surface the original error.
|
||||
}
|
||||
next(error);
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/data-sources/schedules/:id/delete', requirePermission('schedules.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const scheduleGroup = await common.fetchScheduleGroupById(pool, Number(req.params.id));
|
||||
if (!scheduleGroup) {
|
||||
return res.status(404).send('Schedule group not found');
|
||||
}
|
||||
|
||||
const blockMessage = await getDataSourceDeleteBlockMessage('schedule_group_id', scheduleGroup.id, 'schedule group');
|
||||
if (blockMessage) {
|
||||
return res.redirect('/data-sources/schedules?message=' + encodeURIComponent(blockMessage));
|
||||
}
|
||||
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
await connection.query('DELETE FROM i_schedule_groups WHERE id = ?', [scheduleGroup.id]);
|
||||
await connection.commit();
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
res.redirect('/data-sources/schedules?message=' + encodeURIComponent('Schedule group deleted.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user