Release v2.5.6

This commit is contained in:
2026-08-05 19:38:16 +01:00
parent a8ef35b287
commit 9f831f04bc
161 changed files with 8487 additions and 1295 deletions
@@ -0,0 +1,31 @@
// API source duplication helpers.
function buildDuplicateApiSourceName(sourceName) {
const baseName = 'Copy of ' + String(sourceName || '').trim();
return baseName;
}
function buildDuplicateApiSource(apiSource, duplicateName) {
return Object.assign({}, apiSource, {
id: null,
name: duplicateName,
apiUrl: apiSource.api_url || '',
lastPulledAt: null,
lastPullError: '',
lastResponseStatus: null,
lastResponseContentType: '',
lastResponseJson: '',
authMethod: apiSource.auth_method || 'none',
authUsername: apiSource.auth_username || '',
authPassword: apiSource.auth_password || '',
authBearerToken: apiSource.auth_bearer_token || '',
authHeaderName: apiSource.auth_header_name || 'X-API-Key',
authHeaderValue: apiSource.auth_header_value || '',
itemsPath: apiSource.items_path || ''
});
}
module.exports = {
buildDuplicateApiSourceName,
buildDuplicateApiSource
};
@@ -35,8 +35,9 @@ function buildApiSourceFormViewModel(apiSource, message, currentUser, options, i
inUse: Boolean(viewApiSource.inUse),
lastResponseJson: viewOptions.lastResponseJson || viewApiSource.lastResponseJson || '',
lastPullError: viewOptions.lastPullError || viewApiSource.lastPullError || '',
messageVariant: viewOptions.messageVariant || '',
isEdit: Boolean(isEdit),
showSaveSecondaryActions: Boolean(isEdit),
showSaveSecondaryActions: true,
deleteDisabled: !isEdit || Boolean(viewApiSource.inUse),
formAction: isEdit && viewApiSource.id ? '/data-sources/api-sources/' + viewApiSource.id : '/data-sources/api-sources',
formAttrs: isEdit && viewApiSource.id ? 'data-async-save data-async-save-refresh-target="#api-source-response-panel" data-async-save-refresh-state-url="/data-sources/api-sources/' + viewApiSource.id + '/state" data-async-save-close-url="/data-sources/api-sources" data-async-save-new-url="/data-sources/api-sources/new"' : 'data-async-save data-async-save-close-url="/data-sources/api-sources" data-async-save-new-redirect="response-url" data-async-save-new-url="/data-sources/api-sources/new"',
@@ -4,6 +4,7 @@ const { buildPagination } = require('../../../lib/pagination');
const renderApiSourcesPage = require('./list');
const renderApiSourceAddPage = require('./add');
const renderApiSourceEditPage = require('./edit');
const { buildDuplicateApiSourceName, buildDuplicateApiSource } = require('./duplicate');
function toIsoTimestamp(value) {
if (!value) {
@@ -185,6 +186,29 @@ module.exports = function registerApiSourceRoutes(app, deps) {
}
});
app.get('/data-sources/api-sources/:id/duplicate', requirePermission('api-sources.read'), requirePermission('api-sources.create'), async function (req, res, next) {
try {
const apiSource = await common.fetchApiSourceById(pool, Number(req.params.id));
if (!apiSource) {
return res.status(404).send('API source not found');
}
let duplicateName = buildDuplicateApiSourceName(apiSource.name);
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'i_api_sources', duplicateName)) {
duplicateName = buildDuplicateApiSourceName(apiSource.name) + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(renderApiSourceAddPage(buildDuplicateApiSource(apiSource, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
messageVariant: 'info',
showSaveSecondaryActions: true
}));
} catch (error) {
next(error);
}
});
app.get('/data-sources/api-sources/:id/state', requirePermission('api-sources.update'), function (req, res, next) {
sendRefreshTaskState(req, res, dataSourceTasks, Number(req.params.id)).catch(next);
});
@@ -0,0 +1,26 @@
// RSS feed duplication helpers.
function buildDuplicateRssFeedName(feedName) {
const baseName = 'Copy of ' + String(feedName || '').trim();
return baseName;
}
function buildDuplicateRssFeed(rssFeed, duplicateName) {
return Object.assign({}, rssFeed, {
id: null,
name: duplicateName,
feedUrl: rssFeed.feed_url || '',
lastPulledAt: null,
pullError: '',
pulledItems: [],
updateIntervalValue: rssFeed.update_interval_value,
updateIntervalUnit: rssFeed.update_interval_unit || 'minutes',
itemLimit: rssFeed.item_limit,
feedUrlValue: rssFeed.feed_url || ''
});
}
module.exports = {
buildDuplicateRssFeedName,
buildDuplicateRssFeed
};
@@ -24,8 +24,9 @@ function buildRssFeedFormViewModel(rssFeed, message, currentUser, options, isEdi
inUse: Boolean(viewRssFeed.inUse),
pulledItems: viewOptions.pulledItems || viewRssFeed.pulledItems || [],
pullError: viewOptions.pullError || viewRssFeed.pullError || '',
messageVariant: viewOptions.messageVariant || '',
isEdit: Boolean(isEdit),
showSaveSecondaryActions: Boolean(isEdit),
showSaveSecondaryActions: true,
deleteDisabled: !isEdit || Boolean(viewRssFeed.inUse),
formAction: isEdit && viewRssFeed.id ? '/data-sources/rss-feeds/' + viewRssFeed.id : '/data-sources/rss-feeds',
formAttrs: isEdit && viewRssFeed.id ? 'data-async-save data-async-save-refresh-target="#rss-feed-items-panel" data-async-save-refresh-state-url="/data-sources/rss-feeds/' + viewRssFeed.id + '/state" data-async-save-close-url="/data-sources/rss-feeds" data-async-save-new-url="/data-sources/rss-feeds/new"' : 'data-async-save data-async-save-close-url="/data-sources/rss-feeds" data-async-save-new-redirect="response-url" data-async-save-new-url="/data-sources/rss-feeds/new"',
@@ -4,6 +4,7 @@ const { buildPagination } = require('../../../lib/pagination');
const renderRssFeedsPage = require('./list');
const renderRssFeedAddPage = require('./add');
const renderRssFeedEditPage = require('./edit');
const { buildDuplicateRssFeedName, buildDuplicateRssFeed } = require('./duplicate');
async function getDataSourceUsageMaps(pool, common) {
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
@@ -155,6 +156,29 @@ module.exports = function registerRssFeedRoutes(app, deps) {
}
});
app.get('/data-sources/rss-feeds/:id/duplicate', requirePermission('rss-feeds.read'), requirePermission('rss-feeds.create'), async function (req, res, next) {
try {
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
if (!rssFeed) {
return res.status(404).send('RSS feed not found');
}
let duplicateName = buildDuplicateRssFeedName(rssFeed.name);
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'i_rss_feeds', duplicateName)) {
duplicateName = buildDuplicateRssFeedName(rssFeed.name) + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(renderRssFeedAddPage(buildDuplicateRssFeed(rssFeed, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
messageVariant: 'info',
showSaveSecondaryActions: true
}));
} catch (error) {
next(error);
}
});
app.get('/data-sources/rss-feeds/:id/state', requirePermission('rss-feeds.update'), function (req, res, next) {
sendRefreshTaskState(req, res, dataSourceTasks, Number(req.params.id)).catch(next);
});
@@ -3,6 +3,6 @@
const { renderView } = require('../../../view');
const { buildTimetableGroupFormViewModel } = require('./form-view-model');
module.exports = function renderTimetableGroupAddPage(timetableGroup, timetableEntries, message, currentUser) {
return renderView('data-sources/timetables/form', buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, false));
module.exports = function renderTimetableGroupAddPage(timetableGroup, timetableEntries, message, currentUser, options) {
return renderView('data-sources/timetables/form', buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, false, options));
};
@@ -0,0 +1,27 @@
// Timetable duplication helpers.
function buildDuplicateTimetableGroupName(groupName) {
const baseName = 'Copy of ' + String(groupName || '').trim();
return baseName;
}
function buildDuplicateTimetableGroup(timetableGroup, duplicateName) {
return Object.assign({}, timetableGroup, {
id: null,
name: duplicateName,
shortDescription: timetableGroup.short_description || ''
});
}
function buildDuplicateTimetableEntry(timetableEntry) {
return Object.assign({}, timetableEntry, {
id: null,
schedule_group_id: null
});
}
module.exports = {
buildDuplicateTimetableGroupName,
buildDuplicateTimetableGroup,
buildDuplicateTimetableEntry
};
@@ -26,8 +26,9 @@ function formatDateTimeLocalValue(value) {
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
function buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, isEdit) {
function buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, message, currentUser, isEdit, options) {
const viewTimetableGroup = Object.assign(buildDefaultTimetableGroup(), timetableGroup || {});
const viewOptions = options || {};
const viewTimetableEntries = Array.isArray(timetableEntries) && timetableEntries.length
? timetableEntries.map(function (entry) {
return Object.assign({}, entry, {
@@ -42,11 +43,14 @@ function buildTimetableGroupFormViewModel(timetableGroup, timetableEntries, mess
active: 'timetables',
message: message,
currentUser: currentUser || null,
messageVariant: viewOptions.messageVariant || '',
isEdit: Boolean(isEdit),
timetableGroup: viewTimetableGroup,
timetableEntries: viewTimetableEntries,
inUse: Boolean(viewTimetableGroup.inUse),
showSaveSecondaryActions: Boolean(isEdit),
showSaveSecondaryActions: true,
cancelConfirmMessage: "You've made changes. Are you sure you want to leave this page?",
deleteConfirmMessage: isEdit ? 'Delete this timetable group?' : '',
deleteDisabled: !isEdit || Boolean(viewTimetableGroup.inUse),
formAction: isEdit && viewTimetableGroup.id ? '/data-sources/timetables/' + viewTimetableGroup.id : '/data-sources/timetables',
formAttrs: 'data-async-save' + (isEdit ? ' data-async-save-close-url="/data-sources/timetables"' : ' data-async-save-new-redirect="response-url"') + ' data-async-save-new-url="/data-sources/timetables/new"',
@@ -4,6 +4,7 @@ const { buildPagination } = require('../../../lib/pagination');
const renderTimetableGroupsPage = require('./list');
const renderTimetableGroupAddPage = require('./add');
const renderTimetableGroupEditPage = require('./edit');
const { buildDuplicateTimetableGroupName, buildDuplicateTimetableGroup, buildDuplicateTimetableEntry } = require('./duplicate');
async function getDataSourceUsageMaps(pool, common) {
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
@@ -25,8 +26,8 @@ async function getDataSourceUsageMaps(pool, common) {
return;
}
if (Object.prototype.hasOwnProperty.call(value, 'schedule_group_id')) {
const timetableGroupId = Number(value.schedule_group_id);
if (Object.prototype.hasOwnProperty.call(value, 'timetable_group_id')) {
const timetableGroupId = Number(value.timetable_group_id);
if (Number.isFinite(timetableGroupId)) {
timetableGroupIds.add(timetableGroupId);
}
@@ -176,6 +177,32 @@ module.exports = function registerTimetableRoutes(app, deps) {
}
});
app.get('/data-sources/timetables/:id/duplicate', requirePermission('timetables.read'), requirePermission('timetables.create'), async function (req, res, next) {
try {
const timetableGroup = await common.fetchTimetableGroupById(pool, Number(req.params.id));
if (!timetableGroup) {
return res.status(404).send('Timetable group not found');
}
const timetableEntries = typeof common.fetchTimetableEntriesByGroupId === 'function'
? await common.fetchTimetableEntriesByGroupId(pool, timetableGroup.id)
: [];
let duplicateName = buildDuplicateTimetableGroupName(timetableGroup.name);
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'i_schedule_groups', duplicateName)) {
duplicateName = buildDuplicateTimetableGroupName(timetableGroup.name) + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(renderTimetableGroupAddPage(buildDuplicateTimetableGroup(timetableGroup, duplicateName), timetableEntries.map(buildDuplicateTimetableEntry), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
messageVariant: 'info'
}));
} catch (error) {
next(error);
}
});
app.post('/data-sources/timetables', requirePermission('timetables.create'), async function (req, res, next) {
const connection = await pool.getConnection();
try {