From fdbfa63d1776e82c0783d753b16c12f18c1e4e40 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Dec 2024 08:52:13 -0800 Subject: [PATCH 01/13] CMS-338: Add publish tab and backend functionality --- backend/routes/api/parks.js | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index 19a752b..4c73a0d 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -154,4 +154,56 @@ router.get( }), ); +router.get( + "parks/ready-to-publish", + asyncHandler(async (req, res) => { + // const features = await Feature.findAll({ + // attributes: ["id", "name"], + // include: [ + // { + // model: Park, + // as: "park", + // attributes: ["id", "orcs", "name"], + // include: [ + // { + // model: Season, + // as: "seasons", + // attributes: ["id", "status", "readyToPublish"], + // }, + // ], + // }, + // ], + // }); + + // every date range that is approved + // feature - park pairs + // every dateRange in + + const seasons = await Season.findAll({ + attributes: ["id", "status", "readyToPublish"], + include: [ + { + model: Park, + as: "park", + attributes: ["id", "orcs", "name"], + }, + { + model: FeatureType, + as: "featureType", + attributes: ["id", "name"], + }, + { + model: Date, + }, + ], + where: { + readyToPublish: true, + status: "approved", + }, + }); + + res.send("hello"); + }), +); + export default router; From 7b9b6961774d07ce502301dcfb8be962276fae09 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Dec 2024 12:05:11 -0800 Subject: [PATCH 02/13] CMS-338: Update logic to get dates ready to publish --- backend/routes/api/parks.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index 4c73a0d..e4f6508 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -185,7 +185,14 @@ router.get( { model: Park, as: "park", - attributes: ["id", "orcs", "name"], + attributes: ["id", "orcs", "name", "strapiId"], + include: [ + { + model: Feature, + as: "features", + attributes: ["id", "name", "strapiId"], + }, + ], }, { model: FeatureType, From 02d90d10d9ddd05d4a267df9e1cc1f14c5ed5b2b Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Dec 2024 03:48:25 -0800 Subject: [PATCH 03/13] CMS-338: Update fetch logic --- backend/models/dateable.js | 4 +++ backend/models/season.js | 4 +++ backend/routes/api/parks.js | 57 ++++++++++++++++++------------------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/backend/models/dateable.js b/backend/models/dateable.js index 1686c47..0fc01aa 100644 --- a/backend/models/dateable.js +++ b/backend/models/dateable.js @@ -12,6 +12,10 @@ export default (sequelize) => { foreignKey: "dateableId", as: "dateRanges", }); + Dateable.hasMany(models.Feature, { + foreignKey: "dateableId", + as: "feature", + }); } } Dateable.init( diff --git a/backend/models/season.js b/backend/models/season.js index b96a2c1..cdbc144 100644 --- a/backend/models/season.js +++ b/backend/models/season.js @@ -20,6 +20,10 @@ export default (sequelize) => { foreignKey: "seasonId", as: "changeLogs", }); + Season.hasMany(models.DateRange, { + foreignKey: "seasonId", + as: "dateRanges", + }); } } Season.init( diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index e4f6508..f82318b 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -5,8 +5,9 @@ import { Season, FeatureType, Feature, - Dateable, DateRange, + DateType, + Dateable, } from "../../models/index.js"; import asyncHandler from "express-async-handler"; @@ -155,26 +156,8 @@ router.get( ); router.get( - "parks/ready-to-publish", + "/ready-to-publish/", asyncHandler(async (req, res) => { - // const features = await Feature.findAll({ - // attributes: ["id", "name"], - // include: [ - // { - // model: Park, - // as: "park", - // attributes: ["id", "orcs", "name"], - // include: [ - // { - // model: Season, - // as: "seasons", - // attributes: ["id", "status", "readyToPublish"], - // }, - // ], - // }, - // ], - // }); - // every date range that is approved // feature - park pairs // every dateRange in @@ -186,13 +169,6 @@ router.get( model: Park, as: "park", attributes: ["id", "orcs", "name", "strapiId"], - include: [ - { - model: Feature, - as: "features", - attributes: ["id", "name", "strapiId"], - }, - ], }, { model: FeatureType, @@ -200,7 +176,28 @@ router.get( attributes: ["id", "name"], }, { - model: Date, + model: DateRange, + as: "dateRanges", + attributes: ["id", "startDate", "endDate"], + include: [ + { + model: DateType, + as: "dateType", + attributes: ["id", "name"], + }, + { + model: Dateable, + as: "dateable", + attributes: ["id"], + include: [ + { + model: Feature, + as: "feature", + attributes: ["id", "name"], + }, + ], + }, + ], }, ], where: { @@ -209,7 +206,9 @@ router.get( }, }); - res.send("hello"); + const output = seasons.map((season) => season.toJSON()); + + res.send(output); }), ); From 57fc70fd2fb527790f7745f70609fb40581ed7d8 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 18 Dec 2024 15:25:50 -0600 Subject: [PATCH 04/13] CMS-338: refactor endpoints and fetch data from API --- backend/routes/api/parks.js | 154 +++++++++++++++++++--- frontend/src/router/pages/PublishPage.jsx | 18 +-- 2 files changed, 145 insertions(+), 27 deletions(-) diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index f82318b..9547df3 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -8,8 +8,10 @@ import { DateRange, DateType, Dateable, + Campground, } from "../../models/index.js"; import asyncHandler from "express-async-handler"; +import { Op } from "sequelize"; const router = Router(); @@ -158,23 +160,70 @@ router.get( router.get( "/ready-to-publish/", asyncHandler(async (req, res) => { - // every date range that is approved - // feature - park pairs - // every dateRange in + const approvedSeasons = await Season.findAll({ + where: { + status: "approved", + readyToPublish: true, + }, + attributes: ["id", "parkId", "featureTypeId"], + raw: true, + }); + + const parkFeaturePairs = [ + ...new Map( + approvedSeasons.map((season) => [ + `${season.parkId}-${season.featureTypeId}`, // Unique key for the pair + { parkId: season.parkId, featureTypeId: season.featureTypeId }, // Original object + ]), + ).values(), + ]; - const seasons = await Season.findAll({ - attributes: ["id", "status", "readyToPublish"], + const matchingFeatures = await Feature.findAll({ + where: { + [Op.or]: parkFeaturePairs, + }, + attributes: ["id", "name"], include: [ { model: Park, as: "park", - attributes: ["id", "orcs", "name", "strapiId"], + attributes: ["id", "orcs", "name"], }, { - model: FeatureType, - as: "featureType", + model: Campground, + as: "campground", attributes: ["id", "name"], }, + ], + }); + + const features = matchingFeatures.map((feature) => feature.toJSON()); + + // if feature has a campground prepend feature name with campground name + const output = features.map((feature) => { + if (feature.campground) { + feature.name = `${feature.campground.name} - ${feature.name}`; + } + + return feature; + }); + + res.send({ features: output }); + }), +); + +// TODO: make it a post request +// - send data to the API +router.get( + "/publish-to-api/", + asyncHandler(async (req, res) => { + const approvedSeasons = await Season.findAll({ + where: { + status: "approved", + readyToPublish: true, + }, + attributes: ["id", "parkId", "featureTypeId", "operatingYear"], + include: [ { model: DateRange, as: "dateRanges", @@ -193,22 +242,97 @@ router.get( { model: Feature, as: "feature", - attributes: ["id", "name"], + attributes: ["id", "name", "strapiId"], }, ], }, ], }, ], - where: { - readyToPublish: true, - status: "approved", - }, }); - const output = seasons.map((season) => season.toJSON()); + const seasons = approvedSeasons.map((season) => season.toJSON()); + + const table = {}; + + seasons.forEach((season) => { + const { operatingYear } = season; + + if (!table[operatingYear]) { + table[operatingYear] = {}; + } + + const { dateRanges } = season; + + dateRanges.forEach((dateRange) => { + const { dateable } = dateRange; + const { feature } = dateable; + + console.log(feature); + const strapiId = feature[0].strapiId; + + if (!table[operatingYear][strapiId]) { + console.log(strapiId); + table[operatingYear][strapiId] = []; + } + + table[operatingYear][strapiId].push(dateRange); + }); + }); + + const datesToPublish = []; + + console.log(table); + + Object.entries(table).forEach(([operatingYear, features]) => { + Object.entries(features).forEach(([featureId, dateRanges]) => { + const operatingDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Operation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const reservationDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Reservation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const maxIndex = Math.max( + operatingDates.length, + reservationDates.length, + ); + + for (let i = 0; i < maxIndex; i++) { + const obj = { + operatingYear, + parkOperationSubArea: featureId, + isActive: true, + openDate: null, + closeDate: null, + serviceStartDate: null, + serviceEndDate: null, + reservationStartDate: null, + reservationEndDate: null, + offSeasonStartDate: null, + offSeasonEndDate: null, + adminNote: null, + }; + + const operatingDate = operatingDates[i]; + const reservationDate = reservationDates[i]; + + if (operatingDate) { + obj.serviceStartDate = operatingDate.startDate; + obj.serviceEndDate = operatingDate.endDate; + } + + if (reservationDate) { + obj.reservationStartDate = reservationDate.startDate; + obj.reservationEndDate = reservationDate.endDate; + } + datesToPublish.push(obj); + } + }); + }); - res.send(output); + res.json(datesToPublish); }), ); diff --git a/frontend/src/router/pages/PublishPage.jsx b/frontend/src/router/pages/PublishPage.jsx index 598301d..4fb94d4 100644 --- a/frontend/src/router/pages/PublishPage.jsx +++ b/frontend/src/router/pages/PublishPage.jsx @@ -69,18 +69,12 @@ function PublishPage() { - - Golden Ears - Alouette Campground - - - Golden Ears - Gold Creek Campground - - - Golden Ears - North Beach Campground - + {data?.features.map((feature) => ( + + {feature.park.name} + {feature.name} + + ))} From 1e98239d606e4ff75e6cfda34f80d1b2e7797f2d Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 3 Jan 2025 18:10:38 -0600 Subject: [PATCH 05/13] CMS-338: Update endpoints for publishing dates. --- backend/index.js | 2 + backend/routes/api/parks.js | 182 ------------------- backend/routes/api/publish.js | 206 ++++++++++++++++++++++ frontend/src/router/pages/PublishPage.jsx | 12 ++ 4 files changed, 220 insertions(+), 182 deletions(-) create mode 100644 backend/routes/api/publish.js diff --git a/backend/index.js b/backend/index.js index c6faa8a..879fb91 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,6 +11,7 @@ import homeRoutes from "./routes/home.js"; import parkRoutes from "./routes/api/parks.js"; import seasonRoutes from "./routes/api/seasons.js"; import exportRoutes from "./routes/api/export.js"; +import PublishRoutes from "./routes/api/publish.js"; if (!process.env.POSTGRES_SERVER || !process.env.ADMIN_PASSWORD) { throw new Error("Required environment variables are not set"); @@ -58,6 +59,7 @@ const apiRouter = express.Router(); apiRouter.use("/parks", parkRoutes); apiRouter.use("/seasons", seasonRoutes); apiRouter.use("/export", exportRoutes); +apiRouter.use("/publish", PublishRoutes); app.use("/api", checkJwt, apiRouter); diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index 9547df3..9b2c4b2 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -6,12 +6,9 @@ import { FeatureType, Feature, DateRange, - DateType, Dateable, - Campground, } from "../../models/index.js"; import asyncHandler from "express-async-handler"; -import { Op } from "sequelize"; const router = Router(); @@ -157,183 +154,4 @@ router.get( }), ); -router.get( - "/ready-to-publish/", - asyncHandler(async (req, res) => { - const approvedSeasons = await Season.findAll({ - where: { - status: "approved", - readyToPublish: true, - }, - attributes: ["id", "parkId", "featureTypeId"], - raw: true, - }); - - const parkFeaturePairs = [ - ...new Map( - approvedSeasons.map((season) => [ - `${season.parkId}-${season.featureTypeId}`, // Unique key for the pair - { parkId: season.parkId, featureTypeId: season.featureTypeId }, // Original object - ]), - ).values(), - ]; - - const matchingFeatures = await Feature.findAll({ - where: { - [Op.or]: parkFeaturePairs, - }, - attributes: ["id", "name"], - include: [ - { - model: Park, - as: "park", - attributes: ["id", "orcs", "name"], - }, - { - model: Campground, - as: "campground", - attributes: ["id", "name"], - }, - ], - }); - - const features = matchingFeatures.map((feature) => feature.toJSON()); - - // if feature has a campground prepend feature name with campground name - const output = features.map((feature) => { - if (feature.campground) { - feature.name = `${feature.campground.name} - ${feature.name}`; - } - - return feature; - }); - - res.send({ features: output }); - }), -); - -// TODO: make it a post request -// - send data to the API -router.get( - "/publish-to-api/", - asyncHandler(async (req, res) => { - const approvedSeasons = await Season.findAll({ - where: { - status: "approved", - readyToPublish: true, - }, - attributes: ["id", "parkId", "featureTypeId", "operatingYear"], - include: [ - { - model: DateRange, - as: "dateRanges", - attributes: ["id", "startDate", "endDate"], - include: [ - { - model: DateType, - as: "dateType", - attributes: ["id", "name"], - }, - { - model: Dateable, - as: "dateable", - attributes: ["id"], - include: [ - { - model: Feature, - as: "feature", - attributes: ["id", "name", "strapiId"], - }, - ], - }, - ], - }, - ], - }); - - const seasons = approvedSeasons.map((season) => season.toJSON()); - - const table = {}; - - seasons.forEach((season) => { - const { operatingYear } = season; - - if (!table[operatingYear]) { - table[operatingYear] = {}; - } - - const { dateRanges } = season; - - dateRanges.forEach((dateRange) => { - const { dateable } = dateRange; - const { feature } = dateable; - - console.log(feature); - const strapiId = feature[0].strapiId; - - if (!table[operatingYear][strapiId]) { - console.log(strapiId); - table[operatingYear][strapiId] = []; - } - - table[operatingYear][strapiId].push(dateRange); - }); - }); - - const datesToPublish = []; - - console.log(table); - - Object.entries(table).forEach(([operatingYear, features]) => { - Object.entries(features).forEach(([featureId, dateRanges]) => { - const operatingDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Operation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - const reservationDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Reservation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - const maxIndex = Math.max( - operatingDates.length, - reservationDates.length, - ); - - for (let i = 0; i < maxIndex; i++) { - const obj = { - operatingYear, - parkOperationSubArea: featureId, - isActive: true, - openDate: null, - closeDate: null, - serviceStartDate: null, - serviceEndDate: null, - reservationStartDate: null, - reservationEndDate: null, - offSeasonStartDate: null, - offSeasonEndDate: null, - adminNote: null, - }; - - const operatingDate = operatingDates[i]; - const reservationDate = reservationDates[i]; - - if (operatingDate) { - obj.serviceStartDate = operatingDate.startDate; - obj.serviceEndDate = operatingDate.endDate; - } - - if (reservationDate) { - obj.reservationStartDate = reservationDate.startDate; - obj.reservationEndDate = reservationDate.endDate; - } - datesToPublish.push(obj); - } - }); - }); - - res.json(datesToPublish); - }), -); - export default router; diff --git a/backend/routes/api/publish.js b/backend/routes/api/publish.js new file mode 100644 index 0000000..a08f393 --- /dev/null +++ b/backend/routes/api/publish.js @@ -0,0 +1,206 @@ +import { Router } from "express"; +import asyncHandler from "express-async-handler"; +import { Op } from "sequelize"; + +import { + Park, + Season, + Feature, + DateRange, + DateType, + Dateable, + Campground, +} from "../../models/index.js"; + +const router = Router(); + +router.get( + "/ready-to-publish", + asyncHandler(async (req, res) => { + // get all seasons that are approved and ready to be published + const approvedSeasons = await Season.findAll({ + where: { + status: "approved", + readyToPublish: true, + }, + attributes: ["id", "parkId", "featureTypeId"], + raw: true, + }); + + // The frontend needs to display every park-feature pair only once + // even if there are multiple seasons for that pair that are approved and ready to be published + // here we are filtering out duplicates + const parkFeaturePairs = [ + ...new Map( + approvedSeasons.map((season) => [ + `${season.parkId}-${season.featureTypeId}`, // Unique key for the pair + { parkId: season.parkId, featureTypeId: season.featureTypeId }, // Original object + ]), + ).values(), + ]; + + // get all features that are part of the approved and ready to be published seasons + const matchingFeatures = await Feature.findAll({ + where: { + [Op.or]: parkFeaturePairs, + }, + attributes: ["id", "name", "strapiId"], + include: [ + { + model: Park, + as: "park", + attributes: ["id", "orcs", "name"], + }, + { + model: Campground, + as: "campground", + attributes: ["id", "name"], + }, + ], + }); + + const features = matchingFeatures.map((feature) => feature.toJSON()); + + // if feature has a campground prepend feature name with campground name + // some features' names only make sense in the context of a campground + const output = features + .map((feature) => { + if (feature.campground) { + feature.name = `${feature.campground.name} - ${feature.name}`; + } + + return feature; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + + res.send({ features: output }); + }), +); + +// TODO: make it a post request +// - send data to the API +router.get( + "/publish-to-api/", + asyncHandler(async (req, res) => { + // get all seasons that are approved and ready to be published + // and the associated objects we need to build the payload + const approvedSeasons = await Season.findAll({ + where: { + status: "approved", + readyToPublish: true, + }, + attributes: ["id", "parkId", "featureTypeId", "operatingYear"], + include: [ + { + model: DateRange, + as: "dateRanges", + attributes: ["id", "startDate", "endDate"], + include: [ + { + model: DateType, + as: "dateType", + attributes: ["id", "name"], + }, + { + model: Dateable, + as: "dateable", + attributes: ["id"], + include: [ + { + model: Feature, + as: "feature", + attributes: ["id", "name", "strapiId"], + }, + ], + }, + ], + }, + ], + }); + + const seasons = approvedSeasons.map((season) => season.toJSON()); + + // we need to group dateranges by operating year and feature + // The date object in strapi contains both the operating and reservation dates for a feature - operatingYear pair + // we'll group all the date ranges by feature and operating year and then we'll group them if possible + const table = {}; + + seasons.forEach((season) => { + const { operatingYear } = season; + + if (!table[operatingYear]) { + table[operatingYear] = {}; + } + + const { dateRanges } = season; + + dateRanges.forEach((dateRange) => { + const { dateable } = dateRange; + const { feature } = dateable; + + // feature is a list of one element + const strapiId = feature[0].strapiId; + + if (!table[operatingYear][strapiId]) { + table[operatingYear][strapiId] = []; + } + + table[operatingYear][strapiId].push(dateRange); + }); + }); + + const datesToPublish = []; + + Object.entries(table).forEach(([operatingYear, features]) => { + Object.entries(features).forEach(([featureId, dateRanges]) => { + const operatingDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Operation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const reservationDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Reservation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const maxIndex = Math.max( + operatingDates.length, + reservationDates.length, + ); + + for (let i = 0; i < maxIndex; i++) { + const obj = { + operatingYear, + parkOperationSubArea: featureId, + isActive: true, + openDate: null, + closeDate: null, + serviceStartDate: null, + serviceEndDate: null, + reservationStartDate: null, + reservationEndDate: null, + offSeasonStartDate: null, + offSeasonEndDate: null, + adminNote: null, + }; + + const operatingDate = operatingDates[i]; + const reservationDate = reservationDates[i]; + + if (operatingDate) { + obj.serviceStartDate = operatingDate.startDate; + obj.serviceEndDate = operatingDate.endDate; + } + + if (reservationDate) { + obj.reservationStartDate = reservationDate.startDate; + obj.reservationEndDate = reservationDate.endDate; + } + datesToPublish.push(obj); + } + }); + }); + + res.json(datesToPublish); + }), +); + +export default router; diff --git a/frontend/src/router/pages/PublishPage.jsx b/frontend/src/router/pages/PublishPage.jsx index 4fb94d4..2af774f 100644 --- a/frontend/src/router/pages/PublishPage.jsx +++ b/frontend/src/router/pages/PublishPage.jsx @@ -1,8 +1,10 @@ import { useConfirmation } from "@/hooks/useConfirmation"; import { useFlashMessage } from "@/hooks/useFlashMessage"; +import { useApiGet } from "@/hooks/useAPI"; import ConfirmationDialog from "@/components/ConfirmationDialog"; import FlashMessage from "@/components/FlashMessage"; +import LoadingBar from "@/components/LoadingBar"; function PublishPage() { const { @@ -23,6 +25,16 @@ function PublishPage() { isFlashOpen, } = useFlashMessage(); + const { data, loading, error } = useApiGet("/publish/ready-to-publish/"); + + if (loading) { + return ; + } + + if (error) { + return
Error: {error.message}
; + } + async function publishToApi() { const confirm = await openConfirmation( "Publish dates to API?", From 5a1f77364e476d2904e678f8e2c24ecb7beba35b Mon Sep 17 00:00:00 2001 From: Diego Date: Mon, 6 Jan 2025 16:56:41 -0600 Subject: [PATCH 06/13] CMS-338: Updated methods for creating and updating date records in Strapi CMS. --- backend/routes/api/publish.js | 232 ++++++++++++++++++++++++------- backend/routes/api/strapi-api.js | 72 ++++++++++ 2 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 backend/routes/api/strapi-api.js diff --git a/backend/routes/api/publish.js b/backend/routes/api/publish.js index a08f393..b5eee5d 100644 --- a/backend/routes/api/publish.js +++ b/backend/routes/api/publish.js @@ -12,6 +12,8 @@ import { Campground, } from "../../models/index.js"; +import { get, post, put } from "./strapi-api.js"; + const router = Router(); router.get( @@ -77,7 +79,181 @@ router.get( }), ); -// TODO: make it a post request +/** + * Dates added in the staff portal will be sent to the Strapi API - new records will be created + * @param {Array} datesToPublish list of dates that will be published to the API + * @returns {Promise} Promise that resolves when all the records are created in the API + */ +async function createRecordsInStrapi(datesToPublish) { + // create new records in the strapi API + const endpoint = "/api/park-operation-sub-area-dates"; + + return Promise.all( + datesToPublish.map(async (date) => { + try { + const data = { + data: date, + }; + + await post(endpoint, data); + } catch (error) { + console.error( + `Error creating date for featureId ${date.parkOperationSubArea} and year ${date.operatingYear}`, + error, + ); + } + }), + ); +} + +/** + * For each featureId and operatingYear pair, get all the dates from the Strapi API - these will be marked as inactive + * @param {number} featureId id of the feature + * @param {any} operatingYear operating year of the date object + * @returns {Promise} Promise that resolves with the dates from the Strapi API + */ +async function getFeatureStrapiDates(featureId, operatingYear) { + try { + // filter by featureId and operatingYear + const endpoint = `/api/park-operation-sub-area-dates?filters[parkOperationSubArea]=${featureId}&filters[operatingYear]=${operatingYear}`; + + const response = await get(endpoint); + + return response.data; + } catch (error) { + console.error( + `Error fetching dates for featureId ${featureId} and year ${operatingYear}`, + error, + ); + return []; + } +} + +/** + * Mark all the dates for this feature-year pair as inactive + * @param {Array} dates list of dates to be marked as inactive + * @returns {Promise} Promise that resolves when all the dates are marked as inactive + */ +async function markFeatureDatesInactive(dates) { + return Promise.all( + dates.map(async (date) => { + try { + // send the entire object with isActive set to false + const data = { + data: { + ...date.attributes, + isActive: false, + }, + }; + + const endpoint = `/api/park-operation-sub-area-dates/${date.id}`; + const response = await put(endpoint, data); + + return response.data; + } catch (error) { + console.error(`Error marking date ${date.id} as inactive`, error); + return null; + } + }), + ); +} + +/** + * Get all the dates for each feature-year pair and mark them as inactive + * @param {Array} featureYearPairs list of featureId-operatingYear pairs + * @returns {Promise} Promise that resolves when all the dates are marked as inactive + */ +async function markCurrentDatesInactive(featureYearPairs) { + // get all the dates for each feature-year pair + // for each pair send a request to the API to mark the dates as inactive + + return Promise.all( + featureYearPairs.map(async (pair) => { + const { featureId, operatingYear } = pair; + + const dates = await getFeatureStrapiDates(featureId, operatingYear); + + await markFeatureDatesInactive(dates); + }), + ); +} + +/** + * Mark all the current dates for each feature-year pair as inactive and create new records in the Strapi API + * @param {Object} table table of dates grouped by operating year and feature + * @returns {void} + */ +async function publishToAPI(table) { + const datesToPublish = []; + + Object.entries(table).forEach(([operatingYear, features]) => { + Object.entries(features).forEach(([featureId, dateRanges]) => { + // sort operating and reservation dates by start date + const operatingDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Operation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const reservationDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Reservation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + // Since in Strapi, a single date object contains both operating and reservation dates + // we will create an object in Strapi for each pair of operating and reservation dates + // if there are any dates that can't be grouped, they will their own object with null values for the other date types + const maxIndex = Math.max(operatingDates.length, reservationDates.length); + + for (let i = 0; i < maxIndex; i++) { + const obj = { + operatingYear, + parkOperationSubArea: featureId, + isActive: true, + openDate: null, + closeDate: null, + serviceStartDate: null, + serviceEndDate: null, + reservationStartDate: null, + reservationEndDate: null, + offSeasonStartDate: null, + offSeasonEndDate: null, + adminNote: null, + }; + + const operatingDate = operatingDates[i]; + const reservationDate = reservationDates[i]; + + if (operatingDate) { + obj.serviceStartDate = operatingDate.startDate; + obj.serviceEndDate = operatingDate.endDate; + } + + if (reservationDate) { + obj.reservationStartDate = reservationDate.startDate; + obj.reservationEndDate = reservationDate.endDate; + } + datesToPublish.push(obj); + } + }); + }); + + // get all the feature-year pairs from table -- > [ { featureId, operatingYear }, ... ] + const featureYearPairs = Object.entries(table).reduce( + (acc, [operatingYear, features]) => [ + ...acc, + ...Object.keys(features).map((featureId) => ({ + featureId, + operatingYear, + })), + ], + [], + ); + + // mark all the current dates for each feature-year pair as inactive + await markCurrentDatesInactive(featureYearPairs); + + // create new records in the strapi API + await createRecordsInStrapi(datesToPublish); +} + // - send data to the API router.get( "/publish-to-api/", @@ -149,57 +325,11 @@ router.get( }); }); - const datesToPublish = []; - - Object.entries(table).forEach(([operatingYear, features]) => { - Object.entries(features).forEach(([featureId, dateRanges]) => { - const operatingDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Operation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - const reservationDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Reservation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - const maxIndex = Math.max( - operatingDates.length, - reservationDates.length, - ); - - for (let i = 0; i < maxIndex; i++) { - const obj = { - operatingYear, - parkOperationSubArea: featureId, - isActive: true, - openDate: null, - closeDate: null, - serviceStartDate: null, - serviceEndDate: null, - reservationStartDate: null, - reservationEndDate: null, - offSeasonStartDate: null, - offSeasonEndDate: null, - adminNote: null, - }; - - const operatingDate = operatingDates[i]; - const reservationDate = reservationDates[i]; - - if (operatingDate) { - obj.serviceStartDate = operatingDate.startDate; - obj.serviceEndDate = operatingDate.endDate; - } - - if (reservationDate) { - obj.reservationStartDate = reservationDate.startDate; - obj.reservationEndDate = reservationDate.endDate; - } - datesToPublish.push(obj); - } - }); - }); + // will send the data to the API asynchronously - we don't need to wait for the response + publishToAPI(table); - res.json(datesToPublish); + // send 200 OK response with empty body + res.send(); }), ); diff --git a/backend/routes/api/strapi-api.js b/backend/routes/api/strapi-api.js new file mode 100644 index 0000000..774b0a3 --- /dev/null +++ b/backend/routes/api/strapi-api.js @@ -0,0 +1,72 @@ +import axios from "axios"; + +/** + * Get the headers to be used by all requests + * @returns {Object} - Headers for the request + */ +function getHeaders() { + const strapiToken = process.env.STRAPI_TOKEN; + + return { + accept: "application/json", + "Content-Type": "application/json", + authorization: `Bearer ${strapiToken}`, + }; +} + +/** + * Merges the endpoint with the strapi url + * @param {string} endpoint Api endpoint to be called + * @returns {Object} the full url to be called + */ +function getUrl(endpoint) { + const strapiUrl = process.env.STRAPI_URL; + + return `${strapiUrl}${endpoint}`; +} + +/** + * Sends HTTP request to the strapi API + * @param {string} method HTTP method (get, post, put) + * @param {any} endpoint API endpoint to be called + * @param {any} data data to create or update Strapi records + * @returns {Object} http response + */ +async function makeRequest(method, endpoint, data = null) { + const url = getUrl(endpoint); + const headers = getHeaders(); + const config = { headers }; + + const response = await axios({ method, url, data, ...config }); + + return response; +} + +/** + * Get data from the Strapi API + * @param {any} endpoint endpoint to get data from the Strapi API + * @returns {Object} http response + */ +export async function get(endpoint) { + return makeRequest("get", endpoint); +} + +/** + * Create new records in the Strapi API + * @param {any} endpoint endpoint to create new records in the Strapi API + * @param {any} data data to create new records in the Strapi API + * @returns {Object} http response + */ +export async function post(endpoint, data) { + return makeRequest("post", endpoint, data); +} + +/** + * Update Strapi records + * @param {any} endpoint endpoint to update Strapi records + * @param {Object} data data to update Strapi records + * @returns {Object} http response + */ +export async function put(endpoint, data) { + return makeRequest("put", endpoint, data); +} From 05aa83166eac5c8181f7b0155173b2a5808e3784 Mon Sep 17 00:00:00 2001 From: Diego Date: Mon, 6 Jan 2025 17:23:03 -0600 Subject: [PATCH 07/13] CMS-338: Add comments and minor fix. --- backend/routes/api/publish.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/routes/api/publish.js b/backend/routes/api/publish.js index b5eee5d..d8a8109 100644 --- a/backend/routes/api/publish.js +++ b/backend/routes/api/publish.js @@ -65,6 +65,7 @@ router.get( // if feature has a campground prepend feature name with campground name // some features' names only make sense in the context of a campground + // e.g. Alice Lake Campground - Campsites 1-55 const output = features .map((feature) => { if (feature.campground) { @@ -255,7 +256,7 @@ async function publishToAPI(table) { } // - send data to the API -router.get( +router.post( "/publish-to-api/", asyncHandler(async (req, res) => { // get all seasons that are approved and ready to be published @@ -325,7 +326,8 @@ router.get( }); }); - // will send the data to the API asynchronously - we don't need to wait for the response + // will send the data to the API asynchronously + // we don't wait for the response because it can take a while publishToAPI(table); // send 200 OK response with empty body From 6ccce4142d0c590e922a0aa66d3c170a491922ce Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 10 Jan 2025 15:19:43 -0800 Subject: [PATCH 08/13] CMS-338: Update write-back to update data by seasons. --- backend/routes/api/publish.js | 205 ++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/backend/routes/api/publish.js b/backend/routes/api/publish.js index d8a8109..a9687d6 100644 --- a/backend/routes/api/publish.js +++ b/backend/routes/api/publish.js @@ -160,99 +160,104 @@ async function markFeatureDatesInactive(dates) { } /** - * Get all the dates for each feature-year pair and mark them as inactive - * @param {Array} featureYearPairs list of featureId-operatingYear pairs - * @returns {Promise} Promise that resolves when all the dates are marked as inactive + * Marking the season as published in our DB + * @param {number} seasonId the id of the season to mark as published + * @returns {void} */ -async function markCurrentDatesInactive(featureYearPairs) { - // get all the dates for each feature-year pair - // for each pair send a request to the API to mark the dates as inactive - - return Promise.all( - featureYearPairs.map(async (pair) => { - const { featureId, operatingYear } = pair; - - const dates = await getFeatureStrapiDates(featureId, operatingYear); +async function markSeasonPublished(seasonId) { + const season = await Season.findByPk(seasonId); - await markFeatureDatesInactive(dates); - }), - ); + season.status = "published"; + season.save(); } /** * Mark all the current dates for each feature-year pair as inactive and create new records in the Strapi API - * @param {Object} table table of dates grouped by operating year and feature + * @param {Object} seasonTable table of dates grouped by operating year and feature * @returns {void} */ -async function publishToAPI(table) { - const datesToPublish = []; - - Object.entries(table).forEach(([operatingYear, features]) => { - Object.entries(features).forEach(([featureId, dateRanges]) => { - // sort operating and reservation dates by start date - const operatingDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Operation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - const reservationDates = dateRanges - .filter((dateRange) => dateRange.dateType.name === "Reservation") - .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); - - // Since in Strapi, a single date object contains both operating and reservation dates - // we will create an object in Strapi for each pair of operating and reservation dates - // if there are any dates that can't be grouped, they will their own object with null values for the other date types - const maxIndex = Math.max(operatingDates.length, reservationDates.length); - - for (let i = 0; i < maxIndex; i++) { - const obj = { - operatingYear, - parkOperationSubArea: featureId, - isActive: true, - openDate: null, - closeDate: null, - serviceStartDate: null, - serviceEndDate: null, - reservationStartDate: null, - reservationEndDate: null, - offSeasonStartDate: null, - offSeasonEndDate: null, - adminNote: null, - }; - - const operatingDate = operatingDates[i]; - const reservationDate = reservationDates[i]; - - if (operatingDate) { - obj.serviceStartDate = operatingDate.startDate; - obj.serviceEndDate = operatingDate.endDate; - } - - if (reservationDate) { - obj.reservationStartDate = reservationDate.startDate; - obj.reservationEndDate = reservationDate.endDate; +async function publishToAPI(seasonTable) { + // using this instead of Object.entries because we want to only update the data of one season at a time + // to not overload the Strapi API and to make sure that a each season either succeeds or not + for (const [seasonId, { operatingYear, features }] of Object.entries( + seasonTable, + )) { + // everything inside this loop iteration is related to a single season + await Promise.all( + Object.entries(features).map(async ([featureId, dateRanges]) => { + // everything in this loop iteration is related to a single feature in a season + + // mark all the dates for this feature-year pair as inactive in Strapi + const dates = await getFeatureStrapiDates(featureId, operatingYear); + + await markFeatureDatesInactive(dates.data); + + // The date object in strapi contains both the operating and reservation date s for a feature - operatingYear pair + // we'll group all the date ranges by feature and operating year and then we'll group them if possible + // If there are remaining dates, we'll create a new date object with the remaining dates + const operatingDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Operation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + const reservationDates = dateRanges + .filter((dateRange) => dateRange.dateType.name === "Reservation") + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + + // determine how many date objects we need to create + const maxIndex = Math.max( + operatingDates.length, + reservationDates.length, + ); + const groupedSeasonDates = []; + + for (let i = 0; i < maxIndex; i++) { + const obj = { + operatingYear, + parkOperationSubArea: featureId, + isActive: true, + openDate: null, + closeDate: null, + serviceStartDate: null, + serviceEndDate: null, + reservationStartDate: null, + reservationEndDate: null, + offSeasonStartDate: null, + offSeasonEndDate: null, + adminNote: null, + }; + + const operatingDate = operatingDates[i]; + const reservationDate = reservationDates[i]; + + if (operatingDate) { + obj.serviceStartDate = new Date(operatingDate.startDate) + .toISOString() + .split("T")[0]; + obj.serviceEndDate = new Date(operatingDate.endDate) + .toISOString() + .split("T")[0]; + } + + if (reservationDate) { + obj.reservationStartDate = new Date(reservationDate.startDate) + .toISOString() + .split("T")[0]; + obj.reservationEndDate = new Date(reservationDate.endDate) + .toISOString() + .split("T")[0]; + } + + groupedSeasonDates.push(obj); } - datesToPublish.push(obj); - } - }); - }); - - // get all the feature-year pairs from table -- > [ { featureId, operatingYear }, ... ] - const featureYearPairs = Object.entries(table).reduce( - (acc, [operatingYear, features]) => [ - ...acc, - ...Object.keys(features).map((featureId) => ({ - featureId, - operatingYear, - })), - ], - [], - ); - // mark all the current dates for each feature-year pair as inactive - await markCurrentDatesInactive(featureYearPairs); + // add all the dates for this feature in Strapi + await createRecordsInStrapi(groupedSeasonDates); - // create new records in the strapi API - await createRecordsInStrapi(datesToPublish); + // mark this season as published when everything related to it is done + markSeasonPublished(seasonId); + }), + ); + } } // - send data to the API @@ -297,17 +302,27 @@ router.post( const seasons = approvedSeasons.map((season) => season.toJSON()); - // we need to group dateranges by operating year and feature - // The date object in strapi contains both the operating and reservation dates for a feature - operatingYear pair - // we'll group all the date ranges by feature and operating year and then we'll group them if possible - const table = {}; + // we need to group dateranges by season and then feature + // we'll create a table that looks + // { + // seasonId: { + // operatingYear: 2024, + // features: { + // 1: [dateRange, dateRange], + // 2: [dateRange, dateRange], + // }, + // } + // } + + const seasonTable = {}; seasons.forEach((season) => { const { operatingYear } = season; - if (!table[operatingYear]) { - table[operatingYear] = {}; - } + seasonTable[season.id] = { + operatingYear, + features: {}, + }; const { dateRanges } = season; @@ -315,20 +330,18 @@ router.post( const { dateable } = dateRange; const { feature } = dateable; - // feature is a list of one element + // feature is always an array with one element const strapiId = feature[0].strapiId; - if (!table[operatingYear][strapiId]) { - table[operatingYear][strapiId] = []; + if (!seasonTable[season.id].features[strapiId]) { + seasonTable[season.id].features[strapiId] = []; } - table[operatingYear][strapiId].push(dateRange); + seasonTable[season.id].features[strapiId].push(dateRange); }); }); - // will send the data to the API asynchronously - // we don't wait for the response because it can take a while - publishToAPI(table); + publishToAPI(seasonTable); // send 200 OK response with empty body res.send(); From 5de444bd810bcbce18beb94922f65775b59fe603 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 14 Jan 2025 17:20:30 -0800 Subject: [PATCH 09/13] CMS-338: Add minor fixes and update status to on API. --- backend/.env.local.example | 5 +++++ backend/index.js | 4 ++-- backend/routes/api/parks.js | 10 +++++----- backend/routes/api/publish.js | 4 ++-- backend/routes/api/seasons.js | 8 ++++---- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/backend/.env.local.example b/backend/.env.local.example index 3235b8e..417c4b6 100644 --- a/backend/.env.local.example +++ b/backend/.env.local.example @@ -12,3 +12,8 @@ ADMIN_PASSWORD="some random string" ADMIN_COOKIE_NAME="adminjs" ADMIN_COOKIE_PASSWORD="another random string" ADMIN_SESSION_SECRET="another random string" + +# Generate on Strapi admin panel {strapiURL}/admin/settings/api-tokens +STRAPI_TOKEN="your-strapi-token" +# URL of the Strapi API +STRAPI_URL="http://host.docker.internal:1337" diff --git a/backend/index.js b/backend/index.js index 879fb91..477148e 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,7 +11,7 @@ import homeRoutes from "./routes/home.js"; import parkRoutes from "./routes/api/parks.js"; import seasonRoutes from "./routes/api/seasons.js"; import exportRoutes from "./routes/api/export.js"; -import PublishRoutes from "./routes/api/publish.js"; +import publishRoutes from "./routes/api/publish.js"; if (!process.env.POSTGRES_SERVER || !process.env.ADMIN_PASSWORD) { throw new Error("Required environment variables are not set"); @@ -59,7 +59,7 @@ const apiRouter = express.Router(); apiRouter.use("/parks", parkRoutes); apiRouter.use("/seasons", seasonRoutes); apiRouter.use("/export", exportRoutes); -apiRouter.use("/publish", PublishRoutes); +apiRouter.use("/publish", publishRoutes); app.use("/api", checkJwt, apiRouter); diff --git a/backend/routes/api/parks.js b/backend/routes/api/parks.js index 9b2c4b2..ee087df 100644 --- a/backend/routes/api/parks.js +++ b/backend/routes/api/parks.js @@ -5,8 +5,8 @@ import { Season, FeatureType, Feature, - DateRange, Dateable, + DateRange, } from "../../models/index.js"; import asyncHandler from "express-async-handler"; @@ -16,7 +16,7 @@ function getParkStatus(seasons) { // if any season has status==requested, return requested // else if any season has status==pending review, return pending review // else if any season has status==approved, return approved - // if all seasons have status==published, return published + // if all seasons have status==on API, return on API const requested = seasons.some((s) => s.status === "requested"); @@ -36,10 +36,10 @@ function getParkStatus(seasons) { return "approved"; } - const published = seasons.every((s) => s.status === "published"); + const onAPI = seasons.every((s) => s.status === "on API"); - if (published) { - return "published"; + if (onAPI) { + return "on API"; } return null; diff --git a/backend/routes/api/publish.js b/backend/routes/api/publish.js index a9687d6..a91cf9a 100644 --- a/backend/routes/api/publish.js +++ b/backend/routes/api/publish.js @@ -167,7 +167,7 @@ async function markFeatureDatesInactive(dates) { async function markSeasonPublished(seasonId) { const season = await Season.findByPk(seasonId); - season.status = "published"; + season.status = "on API"; season.save(); } @@ -192,7 +192,7 @@ async function publishToAPI(seasonTable) { await markFeatureDatesInactive(dates.data); - // The date object in strapi contains both the operating and reservation date s for a feature - operatingYear pair + // The date object in strapi contains both the operating and reservation dates for a feature - operatingYear pair // we'll group all the date ranges by feature and operating year and then we'll group them if possible // If there are remaining dates, we'll create a new date object with the remaining dates const operatingDates = dateRanges diff --git a/backend/routes/api/seasons.js b/backend/routes/api/seasons.js index 106e650..53b3e81 100644 --- a/backend/routes/api/seasons.js +++ b/backend/routes/api/seasons.js @@ -22,14 +22,14 @@ const router = Router(); // // rn we're just setting everything to requested // // For staff // // requested -- > requested -// // pending review -- > pending review -// // approved -- > pending review -// // published --> pending review +// // under review -- > under review +// // approved -- > under review +// // on API --> under review // // For operator // // requested -- > requested // // pending review -- > requested // // approved -- > requested -// // published --> requested +// // on API --> requested // return season.status; // } From 9c2c0ff77a107ad9bb7a790b56d3b069b59d69ea Mon Sep 17 00:00:00 2001 From: Diego Ramirez Date: Thu, 16 Jan 2025 09:27:51 -0800 Subject: [PATCH 10/13] CMS-338: Change status from published to on API. --- backend/routes/api/seasons.js | 6 +++--- frontend/src/components/ParkDetailsSeason.jsx | 2 +- frontend/src/components/StatusBadge.jsx | 2 +- frontend/src/hooks/useValidation.js | 2 +- frontend/src/router/pages/EditAndReview.jsx | 2 +- frontend/src/router/pages/SubmitDates.jsx | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/routes/api/seasons.js b/backend/routes/api/seasons.js index 53b3e81..3030dfe 100644 --- a/backend/routes/api/seasons.js +++ b/backend/routes/api/seasons.js @@ -22,9 +22,9 @@ const router = Router(); // // rn we're just setting everything to requested // // For staff // // requested -- > requested -// // under review -- > under review -// // approved -- > under review -// // on API --> under review +// // pending review -- > pending review +// // approved -- > pending review +// // on API --> pending review // // For operator // // requested -- > requested // // pending review -- > requested diff --git a/frontend/src/components/ParkDetailsSeason.jsx b/frontend/src/components/ParkDetailsSeason.jsx index 0babc3e..464adbb 100644 --- a/frontend/src/components/ParkDetailsSeason.jsx +++ b/frontend/src/components/ParkDetailsSeason.jsx @@ -110,7 +110,7 @@ export default function ParkSeason({ season }) { if (confirm) { navigate(`/park/${parkId}/edit/${season.id}`); } - } else if (season.status === "published") { + } else if (season.status === "on API") { const confirm = openConfirmation( "Edit published dates?", "Dates will need to be reviewed again to be approved and published. If reservations have already begun, visitors will be affected.", diff --git a/frontend/src/components/StatusBadge.jsx b/frontend/src/components/StatusBadge.jsx index eee41a8..aa7286a 100644 --- a/frontend/src/components/StatusBadge.jsx +++ b/frontend/src/components/StatusBadge.jsx @@ -12,7 +12,7 @@ export default function StatusBadge({ status }) { // Map status code to color class and display label const statusMap = new Map([ - ["published", { cssClass: "text-bg-primary", displayText: "Published" }], + ["on API", { cssClass: "text-bg-primary", displayText: "on API" }], ["approved", { cssClass: "text-bg-success", displayText: "Approved" }], ["requested", { cssClass: "text-bg-warning", displayText: "Requested" }], ]); diff --git a/frontend/src/hooks/useValidation.js b/frontend/src/hooks/useValidation.js index 8d1a44c..4534840 100644 --- a/frontend/src/hooks/useValidation.js +++ b/frontend/src/hooks/useValidation.js @@ -61,7 +61,7 @@ export default function useValidation(dates, notes, season) { function validateNotes(value = notes) { clearError("notes"); - if (!value && ["approved", "published"].includes(season.status)) { + if (!value && ["approved", "on API"].includes(season.status)) { return addError( "notes", "The dates you are editing have already been Approved or Published. Please provide a note explaining the reason for this update.", diff --git a/frontend/src/router/pages/EditAndReview.jsx b/frontend/src/router/pages/EditAndReview.jsx index d33ba26..f7ffb2f 100644 --- a/frontend/src/router/pages/EditAndReview.jsx +++ b/frontend/src/router/pages/EditAndReview.jsx @@ -16,7 +16,7 @@ function EditAndReview() { { value: "pending review", label: "Pending review" }, { value: "requested", label: "Requested" }, { value: "approved", label: "Approved" }, - { value: "published", label: "Published" }, + { value: "on API", label: "On API" }, ]; const statusValues = statusOptions.map((option) => option.value); diff --git a/frontend/src/router/pages/SubmitDates.jsx b/frontend/src/router/pages/SubmitDates.jsx index 9639c2d..dd3b000 100644 --- a/frontend/src/router/pages/SubmitDates.jsx +++ b/frontend/src/router/pages/SubmitDates.jsx @@ -128,7 +128,7 @@ function SubmitDates() { } async function submitChanges(savingDraft = false) { - if (["pending review", "approved", "published"].includes(season.status)) { + if (["pending review", "approved", "on API"].includes(season.status)) { const confirm = await openConfirmation( "Move back to draft?", "The dates will be moved back to draft and need to be submitted again to be reviewed.", @@ -708,7 +708,7 @@ function SubmitDates() {

Notes - {["approved", "published"].includes(season?.status) && ( + {["approved", "on API"].includes(season?.status) && ( * )}

From 818c13f6a9bb7fe2de074ae0bf25fd63190f9083 Mon Sep 17 00:00:00 2001 From: Diego Ramirez Date: Thu, 16 Jan 2025 11:52:15 -0800 Subject: [PATCH 11/13] CMS-338: Update strapi URL to use env var during sync. --- backend/strapi-sync/sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/strapi-sync/sync.js b/backend/strapi-sync/sync.js index 475442e..11f39f5 100644 --- a/backend/strapi-sync/sync.js +++ b/backend/strapi-sync/sync.js @@ -76,7 +76,7 @@ export async function getData(url, queryParams) { * @returns {Array} list of all models with thier name, endpoint, and items */ export async function fetchAllModels() { - const url = "https://cms.bcparks.ca/api"; + const url = `${process.env.STRAPI_TOKEN}/api`; const strapiData = [ { @@ -499,7 +499,7 @@ export async function syncData() { */ export async function oneTimeDataImport() { // only meant to run once - not needed for regular sync - const url = "https://cms.bcparks.ca/api"; + const url = `${process.env.STRAPI_TOKEN}/api`; const datesData = { endpoint: "/park-operation-sub-area-dates", From d7f08e5c7b606345e66a4234b6138fb108791ac3 Mon Sep 17 00:00:00 2001 From: Diego Ramirez Date: Thu, 16 Jan 2025 13:26:04 -0800 Subject: [PATCH 12/13] CMS-338: Fix error. Incorrect env var was being used for syncing. --- backend/strapi-sync/sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/strapi-sync/sync.js b/backend/strapi-sync/sync.js index 11f39f5..adc3617 100644 --- a/backend/strapi-sync/sync.js +++ b/backend/strapi-sync/sync.js @@ -76,7 +76,7 @@ export async function getData(url, queryParams) { * @returns {Array} list of all models with thier name, endpoint, and items */ export async function fetchAllModels() { - const url = `${process.env.STRAPI_TOKEN}/api`; + const url = `${process.env.STRAPI_URL}/api`; const strapiData = [ { @@ -499,7 +499,7 @@ export async function syncData() { */ export async function oneTimeDataImport() { // only meant to run once - not needed for regular sync - const url = `${process.env.STRAPI_TOKEN}/api`; + const url = `${process.env.STRAPI_URL}/api`; const datesData = { endpoint: "/park-operation-sub-area-dates", From ef526e94bdb9de950149240380f7ee810185a3e6 Mon Sep 17 00:00:00 2001 From: Duncan MacKenzie Date: Thu, 16 Jan 2025 14:36:16 -0800 Subject: [PATCH 13/13] CMS-338: Use .env files if they exist for all npm scripts --- backend/env.js | 10 ++++ backend/index.js | 1 + backend/package-lock.json | 59 +++++++------------ backend/package.json | 4 +- .../create-multiple-item-campgrounds.js | 1 + .../create-single-item-campgrounds.js | 1 + backend/strapi-sync/sync.js | 1 + 7 files changed, 36 insertions(+), 41 deletions(-) create mode 100644 backend/env.js diff --git a/backend/env.js b/backend/env.js new file mode 100644 index 0000000..8c1eb4e --- /dev/null +++ b/backend/env.js @@ -0,0 +1,10 @@ +import dotenvx from "@dotenvx/dotenvx"; + +// Load local .env files, if they exist. +dotenvx.config({ + path: [".env", ".env.local"], + overload: true, + // Ignore missing file warnings: files are just for dev + // and any files loaded will print a success message + ignore: ["MISSING_ENV_FILE"], +}); diff --git a/backend/index.js b/backend/index.js index 477148e..d7e8a42 100644 --- a/backend/index.js +++ b/backend/index.js @@ -5,6 +5,7 @@ import helmet from "helmet"; import compression from "compression"; import RateLimit from "express-rate-limit"; +import "./env.js"; import checkJwt from "./middleware/checkJwt.js"; import { admin, adminRouter, sessionMiddleware } from "./middleware/adminJs.js"; import homeRoutes from "./routes/home.js"; diff --git a/backend/package-lock.json b/backend/package-lock.json index 49087e2..fef8b73 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@adminjs/express": "^6.1.0", "@adminjs/sequelize": "^4.1.1", + "@dotenvx/dotenvx": "^1.33.0", "@fast-csv/format": "^5.0.2", "adminjs": "^7.8.13", "axios": "^1.7.8", @@ -35,7 +36,6 @@ "tslib": "^2.7.0" }, "devDependencies": { - "@dotenvx/dotenvx": "^1.14.0", "eslint": "^9.9.1", "eslint-config-eslint": "^11.0.0", "eslint-config-prettier": "^9.1.0", @@ -1770,10 +1770,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.32.0.tgz", - "integrity": "sha512-oQaGYijYfQx6pY9D+FQ08gUOckF1R0RSVK7Jqk+Ma2RyeceoMIawQl1KoogRaJ12i0SmyVWhiGyQxDU01/k13g==", - "dev": true, + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.33.0.tgz", + "integrity": "sha512-fWVhSrdtObkRJ5SwyNSEUPPm5BHXGlQJAbXeJfrcnonSVdMhKG9pihvJWv86sv8uR0sF/Yd0oI+a9Mj3ISgM3Q==", "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", @@ -1798,7 +1797,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz", "integrity": "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==", - "dev": true, "license": "MIT", "engines": { "bun": ">=1", @@ -2437,9 +2435,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2487,9 +2485,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", "dev": true, "license": "MIT", "engines": { @@ -2507,12 +2505,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -2777,7 +2776,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.0.tgz", "integrity": "sha512-YGdEUzYEd+82jeaVbSKKVp1jFZb8LwaNMIIzHFkihGvYdd/KKAr7KaJHdEdSYGredE3ssSravXIa0Jxg28Sv5w==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2790,7 +2788,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "dev": true, "license": "MIT", "dependencies": { "@noble/hashes": "1.7.0" @@ -2806,7 +2803,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -5059,7 +5055,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -5450,7 +5445,6 @@ "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5498,7 +5492,6 @@ "version": "0.4.13", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz", "integrity": "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==", - "dev": true, "license": "MIT", "dependencies": { "@ecies/ciphers": "^0.2.2", @@ -5799,19 +5792,19 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", + "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.18.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -6311,7 +6304,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -6543,7 +6535,6 @@ "version": "6.4.2", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -6873,7 +6864,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -7080,7 +7070,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -7163,7 +7152,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7400,7 +7388,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7425,7 +7412,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -8088,7 +8074,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, "license": "MIT" }, "node_modules/methods": { @@ -8418,7 +8403,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -8452,7 +8436,6 @@ "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -10644,7 +10627,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11244,7 +11226,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" diff --git a/backend/package.json b/backend/package.json index e15b66a..248face 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "dev": "dotenvx run -f .env.local --overload -- nodemon index.js", + "dev": "nodemon index.js", "start": "node index.js", "lint": "eslint .", "migrate": "sequelize-cli db:migrate", @@ -18,7 +18,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@dotenvx/dotenvx": "^1.14.0", "eslint": "^9.9.1", "eslint-config-eslint": "^11.0.0", "eslint-config-prettier": "^9.1.0", @@ -27,6 +26,7 @@ "prettier": "3.3.3" }, "dependencies": { + "@dotenvx/dotenvx": "^1.33.0", "@adminjs/express": "^6.1.0", "@adminjs/sequelize": "^4.1.1", "@fast-csv/format": "^5.0.2", diff --git a/backend/strapi-sync/create-multiple-item-campgrounds.js b/backend/strapi-sync/create-multiple-item-campgrounds.js index eb95a37..06d58fb 100644 --- a/backend/strapi-sync/create-multiple-item-campgrounds.js +++ b/backend/strapi-sync/create-multiple-item-campgrounds.js @@ -1,3 +1,4 @@ +import "../env.js"; import { Campground, Feature, Park } from "../models/index.js"; import { getItemByAttributes, createModel } from "./utils.js"; diff --git a/backend/strapi-sync/create-single-item-campgrounds.js b/backend/strapi-sync/create-single-item-campgrounds.js index e3ba486..c5be075 100644 --- a/backend/strapi-sync/create-single-item-campgrounds.js +++ b/backend/strapi-sync/create-single-item-campgrounds.js @@ -1,3 +1,4 @@ +import "../env.js"; import { Campground, Feature, Park } from "../models/index.js"; import { getItemByAttributes, createModel } from "./utils.js"; diff --git a/backend/strapi-sync/sync.js b/backend/strapi-sync/sync.js index adc3617..a60f338 100644 --- a/backend/strapi-sync/sync.js +++ b/backend/strapi-sync/sync.js @@ -1,3 +1,4 @@ +import "../env.js"; import { get } from "./axios.js"; import { getItemByAttributes,