From 0212230705293083799a9f4a655f0b8e46c9be3b Mon Sep 17 00:00:00 2001 From: Jacqueline Date: Fri, 6 Dec 2024 13:56:31 -0500 Subject: [PATCH 1/6] Add S/U --- client/src/modules/Course/Components/ReviewModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/modules/Course/Components/ReviewModal.tsx b/client/src/modules/Course/Components/ReviewModal.tsx index 6f168cce..6684d5d6 100644 --- a/client/src/modules/Course/Components/ReviewModal.tsx +++ b/client/src/modules/Course/Components/ReviewModal.tsx @@ -43,7 +43,9 @@ const ReviewModal = ({ 'D+', 'D', 'D-', - 'F' + 'F', + 'S', + 'U', ]; // Form & Review Content State From 38f76669d020c25b5ba2a98f281855370233aef6 Mon Sep 17 00:00:00 2001 From: Jacqueline Date: Sun, 8 Dec 2024 16:36:13 -0500 Subject: [PATCH 2/6] Add admin backend documentation --- server/src/admin/admin.controller.ts | 11 ++++++++ server/src/admin/admin.data-access.ts | 36 ++++++++++++++++++++++++--- server/src/admin/admin.router.ts | 8 ++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index 2907dc14..96d0577f 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -96,6 +96,11 @@ export const editReviewVisibility = async ({ return false; }; +/** + * Approves all pending reviews + * @param {Auth} auth: Object that represents the authentication of a request being passed in. + * @returns Result of the approval process or an error message. + */ export const approveReviews = async ({ auth }: VerifyAdminType) => { const userIsAdmin = await verifyTokenAdmin({ auth }); if (userIsAdmin) { @@ -247,6 +252,12 @@ export const updateAllProfessorsDb = async ({ auth }: VerifyAdminType) => { return result; }; +/** + * Resets all professor arrays in the database to empty arrays + * + * @param {Auth} auth: Object that represents the authentication of a request being passed in. + * @returns true if operation was successful, false if operations was not successful, null if token not admin + */ export const resetAllProfessorsDb = async ({ auth }: VerifyAdminType) => { const userIsAdmin = verifyTokenAdmin({ auth }); if (!userIsAdmin) { diff --git a/server/src/admin/admin.data-access.ts b/server/src/admin/admin.data-access.ts index 7b834f50..b19ccece 100644 --- a/server/src/admin/admin.data-access.ts +++ b/server/src/admin/admin.data-access.ts @@ -3,6 +3,11 @@ import { Classes, ReviewDocument, Reviews, Students } from '../../db/schema'; import { UpdateCourseMetrics } from './admin.type'; import { findCourseById } from '../course/course.data-access'; +/** + * Updates metrics for a course based on a recently written review + * @param review the review that was submitted by a user and approved by an admin + * @param state the updated metrics for the specified course + */ export const updateCourseMetrics = async ( review: ReviewDocument, state: UpdateCourseMetrics @@ -25,7 +30,7 @@ export const updateCourseMetrics = async ( }; /* - * Function to return all pending reviews in the database. + * Returns all pending reviews in the database */ export const findPendingReviews = async () => await Reviews.find( @@ -35,7 +40,7 @@ export const findPendingReviews = async () => ).exec(); /* - * Function to return all reported reviews in the database. + * Returns all reported reviews in the database */ export const findReportedReviews = async () => await Reviews.find( @@ -45,7 +50,7 @@ export const findReportedReviews = async () => ).exec(); /* - * Function to count reviews by approved, pending, and reported and return the values. + * Count reviews by approved, pending, and reported and return the total counts */ export const findReviewCounts = async () => { const approvedCount = await Reviews.countDocuments({ visible: 1 }).exec(); @@ -66,7 +71,7 @@ export const findReviewCounts = async () => { }; /* - * Function to count the number of approved reviews per class in the database. + * Counts the number of approved reviews per class in the database. * Count per class is mapped to a CSV string format. */ export const createCourseCSV = async () => { @@ -95,10 +100,20 @@ export const createCourseCSV = async () => { return csv; }; +/** + * Removes pending review from website and database + * @param {string} reviewId: Mongo generated review id. + */ export const removeReviewById = async (reviewId: string) => { await Reviews.deleteOne({ _id: reviewId }); }; +/** + * Updates review visibility on website and profile page + * @param {string} reviewId: Mongo generated review id. + * @param {number} reported: 1 review was reported, 0 otherwise. + * @param {number} visible: 1 if want to set review to visible to public, 0 if review is only visible by admin. + */ export const updateReviewVisibility = async ( reviewId: string, reported: number, @@ -107,6 +122,9 @@ export const updateReviewVisibility = async ( await Reviews.updateOne({ _id: reviewId }, { $set: { visible, reported } }); }; +/** + * Approves all pending reviews at once + */ export const approveAllReviews = async () => { await Reviews.updateMany( { visible: 0, reported: 0 }, @@ -125,6 +143,11 @@ export const findAdminUsers = async () => { return adminUsers; }; +/** + * Removes admin privilege from a user + * @param id netid of user + * @returns result of the database operation + */ export const removeAdminPrivilege = async (id: string) => { const res = await Students.updateOne( { netId: id }, @@ -133,6 +156,11 @@ export const removeAdminPrivilege = async (id: string) => { return res; }; +/** + * Gives a specified user admin privilege + * @param id netid of user + * @returns result of the database operation + */ export const grantAdminPrivilege = async (id: string) => { const res = await Students.updateOne( { netId: id }, diff --git a/server/src/admin/admin.router.ts b/server/src/admin/admin.router.ts index 7ad44639..bc8ab405 100644 --- a/server/src/admin/admin.router.ts +++ b/server/src/admin/admin.router.ts @@ -420,6 +420,10 @@ adminRouter.post('/professors/reset', async (req, res) => { } }); +/** Reachable at POST /api/admin/course/desc + * @body token: a session's current token + * Updates all courses in the db with their course descriptions. For admins only + */ adminRouter.post('/course/desc', async (req, res) => { const { token }: AdminRequestType = req.body; try { @@ -442,6 +446,10 @@ adminRouter.post('/course/desc', async (req, res) => { } }); +/** Reachable at POST /api/admin/subjects/update + * @body token: a session's current token + * Updates all subjects in the db with their full subject names. For admins only + */ adminRouter.post('/subjects/update', async (req, res) => { const { token }: AdminRequestType = req.body; try { From 8a693c918295a1dae0c27716e2de29e597a9a776 Mon Sep 17 00:00:00 2001 From: Jacqueline Date: Sun, 8 Dec 2024 16:41:11 -0500 Subject: [PATCH 3/6] Add documentation frontend --- client/src/modules/Admin/Components/Admin.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/modules/Admin/Components/Admin.tsx b/client/src/modules/Admin/Components/Admin.tsx index 6bb74826..0787f688 100644 --- a/client/src/modules/Admin/Components/Admin.tsx +++ b/client/src/modules/Admin/Components/Admin.tsx @@ -55,6 +55,9 @@ export const Admin = () => { const [loading, setLoading] = useState(true); const [isAdmin, setIsAdmin] = useState(false); + /** + * Confirms if the logged-in user is an admin + */ useEffect(() => { async function confirmAdmin() { const response = await axios.post(`/api/admin/token/validate`, { @@ -334,6 +337,10 @@ export const Admin = () => { } } + /** + * Call when admin wants to update the similar courses data when clicking the + * "Update Similarity Data" button + */ async function updateSimilarityData() { console.log('Updatng course similarity data') setUpdating(true) From 8acc61002b9d0da5f28ed0c76c47bcdf17766d84 Mon Sep 17 00:00:00 2001 From: Jacqueline Date: Sun, 8 Dec 2024 16:54:53 -0500 Subject: [PATCH 4/6] Update all functions to const arrow functions --- client/src/modules/Admin/Components/Admin.tsx | 66 ++++++++++--------- .../modules/Admin/Components/AdminReview.tsx | 4 +- .../Admin/Components/ManageAdminModal.tsx | 10 +-- client/src/modules/Admin/Components/Stats.tsx | 4 +- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/client/src/modules/Admin/Components/Admin.tsx b/client/src/modules/Admin/Components/Admin.tsx index 0787f688..891cbbc0 100644 --- a/client/src/modules/Admin/Components/Admin.tsx +++ b/client/src/modules/Admin/Components/Admin.tsx @@ -31,6 +31,7 @@ export const Admin = () => { | 'subjects' | 'database' | 'description' + | 'similarity' | 'summarize' | 'failure'; const [updated, setUpdated] = useState('empty'); @@ -42,6 +43,7 @@ export const Admin = () => { subjects: 'Subject full name data successfully updated', database: 'Database successfully initialized', description: 'Course description data successfully added', + similarity: 'Similarity data successfully added', summarize: 'All courses successfully summarized', failure: 'API failed' }; @@ -59,7 +61,7 @@ export const Admin = () => { * Confirms if the logged-in user is an admin */ useEffect(() => { - async function confirmAdmin() { + const confirmAdmin = async () => { const response = await axios.post(`/api/admin/token/validate`, { token: token }); @@ -79,7 +81,7 @@ export const Admin = () => { * pending (awaiting approval), and reported (hidden and awaiting approval) */ useEffect(() => { - async function loadReviews() { + const loadReviews = async () => { const pending = await axios.post('/api/admin/reviews/get-pending', { token: token }); @@ -100,7 +102,7 @@ export const Admin = () => { * Helper function to remove a review from a list of reviews and * return the updated list */ - function removeReviewFromList(reviewToRemove: Review, reviews: Review[]) { + const removeReviewFromList = (reviewToRemove: Review, reviews: Review[]) => { reviews = reviews.filter((review: Review) => { return review && review._id !== reviewToRemove._id; }); @@ -111,7 +113,7 @@ export const Admin = () => { * Call when user asks to approve a review. Accesses the Reviews database * and changes the review with this id to visible. */ - async function approveReview(review: Review) { + const approveReview = async (review: Review) => { const response = await axios.post('/api/admin/reviews/approve', { review: review, token: token @@ -130,7 +132,7 @@ export const Admin = () => { * Call when user asks to remove a review. Accesses the Reviews database * and deletes the review with this id. */ - async function removeReview(review: Review, isUnapproved: boolean) { + const removeReview = async (review: Review, isUnapproved: boolean) => { try { const response = await axios.post('/api/admin/reviews/remove', { review: review, @@ -159,7 +161,7 @@ export const Admin = () => { /** * Call when admin would like to mass-approve all of the currently pending reviews. */ - async function approveAllReviews(reviews: Review[]) { + const approveAllReviews = async () => { const response = await axios.post('/api/admin/reviews/approve/all', { token: token }); @@ -170,10 +172,12 @@ export const Admin = () => { } } - // Call when user selects "Sumarize Reviews" button. Calls endpoint to generate - // summaries and tags using AI for all courses with a freshness above a certain - // threshold, then updates those courses to include these new summaries and tags. - async function summarizeReviews() { + /** + * Call when user selects "Sumarize Reviews" button. Calls endpoint to generate + * summaries and tags using AI for all courses with a freshness above a certain + * threshold, then updates those courses to include these new summaries and tags. + */ + const summarizeReviews = async () => { console.log('Updating all courses with AI'); setUpdating(true); setUpdatingField('summarizing'); @@ -191,7 +195,7 @@ export const Admin = () => { * Call when user asks to un-report a reported review. Accesses the Reviews database * and changes the reported flag for this review to false. */ - async function unReportReview(review: Review) { + const unReportReview = async (review: Review) => { const response = await axios.post('/api/admin/reviews/restore', { review: review, token: token @@ -211,7 +215,7 @@ export const Admin = () => { * course API for new classes and updates classes existing in the database. * Should run once a semester, when new classes are added to the roster. */ - async function addNewSemesters(semesters: string[]) { + const addNewSemesters = async (semesters: string[]) => { console.log('Adding new semester...'); setUpdating(true); setUpdatingField('new semesters'); @@ -233,14 +237,16 @@ export const Admin = () => { setUpdated('semester'); } - // Call when user selects "Initialize Database" button. Scrapes the Cornell - // Course API to store all classes and subjects in the local database. - // Then, runs code to store id's of cross-listed classes against each class. - // Should only be run ONCE when the app is initialzied. - // - // NOTE: requires an initialize flag to ensure the function is only run on - // a button click without this, it will run every time this component is created. - async function addAllCourses() { + /** + * Call when user selects "Initialize Database" button. Scrapes the Cornell + * Course API to store all classes and subjects in the local database. + * Then, runs code to store id's of cross-listed classes against each class. + * Should only be run ONCE when the app is initialzied. + * + * NOTE: requires an initialize flag to ensure the function is only run on + * a button click without this, it will run every time this component is created. + */ + const addAllCourses = async () => { console.log('Initializing database'); setUpdating(true); setUpdatingField('all database'); @@ -260,7 +266,7 @@ export const Admin = () => { * Call when admin wants to update professors for users to search through * when clicking the "Update Professors" button */ - async function updateProfessors() { + const updateProfessors = async () => { console.log('Updating professors'); setUpdating(true); setUpdatingField('professors'); @@ -281,7 +287,7 @@ export const Admin = () => { * Call when admin wants to reset all professors in classes when clicking the * "Reset Professors" button */ - async function resetProfessors() { + const resetProfessors = async () => { console.log('Setting the professors to an empty array'); setUpdating(true); setUpdatingField('professors to empty arrays'); @@ -302,7 +308,7 @@ export const Admin = () => { * Call when user selects "Update Descriptions" button. Scrapes the Course API * to retrieve course description and stores them in the Course database. */ - async function updateDescriptions() { + const updateDescriptions = async () => { console.log('Updating course descriptions'); setUpdating(true); setUpdatingField('course descriptions'); @@ -322,7 +328,7 @@ export const Admin = () => { * Call when admin wants to update the list of subjects users can search through * when clicking the "Update Subjects" button */ - async function updateSubjects() { + const updateSubjects = async () => { setUpdating(true); setUpdatingField('subjects'); const response = await axios.post('/api/admin/subjects/update', { @@ -341,7 +347,7 @@ export const Admin = () => { * Call when admin wants to update the similar courses data when clicking the * "Update Similarity Data" button */ - async function updateSimilarityData() { + const updateSimilarityData = async () => { console.log('Updatng course similarity data') setUpdating(true) setUpdatingField("course similarity data") @@ -359,7 +365,7 @@ export const Admin = () => { * Handle the first click to the "Initialize Database" button. Show an alert * and update state to remember the next click will be a double click. */ - function firstClickHandler() { + const firstClickHandler = () => { alert( '

Warning!

Clicking again will reset all data in the database. Are you sure you want to do this?

' ); @@ -372,7 +378,7 @@ export const Admin = () => { * If this is the user's second click, call addAllCourses above to initiaize * the local database */ - function renderInitButton() { + const renderInitButton = () => { // Offer button to edit database if (doubleClick) { return ( @@ -403,7 +409,7 @@ export const Admin = () => { } } - function renderAdmin(userToken: string) { + const renderAdmin = (userToken: string) => { return (
@@ -497,7 +503,7 @@ export const Admin = () => { @@ -535,7 +541,7 @@ export const Admin = () => { ); } - function adminLogin() { + const adminLogin = () => { if (loading) { return ; } else if (isLoggedIn && token && isAdmin) { diff --git a/client/src/modules/Admin/Components/AdminReview.tsx b/client/src/modules/Admin/Components/AdminReview.tsx index 903f650c..43012a0f 100644 --- a/client/src/modules/Admin/Components/AdminReview.tsx +++ b/client/src/modules/Admin/Components/AdminReview.tsx @@ -27,7 +27,7 @@ const UpdateReview = ({ const [shortName, setShortName] = useState(''); const [fullName, setFullName] = useState(''); - async function getCourse() { + const getCourse = async () => { const response = await axios.post(`/api/courses/get-by-id`, { courseId: review.class }); @@ -41,7 +41,7 @@ const UpdateReview = ({ getCourse(); - function renderButtons(adminReview: any) { + const renderButtons = (adminReview: any) => { const reported = adminReview.reported; if (reported === 1) { return ( diff --git a/client/src/modules/Admin/Components/ManageAdminModal.tsx b/client/src/modules/Admin/Components/ManageAdminModal.tsx index ab7718b6..b8170cc9 100644 --- a/client/src/modules/Admin/Components/ManageAdminModal.tsx +++ b/client/src/modules/Admin/Components/ManageAdminModal.tsx @@ -16,7 +16,7 @@ const ManageAdminModal = ({ token, open, setOpen }: Props) => { const [admins, setAdmins] = useState([]); const [netId, setNetId] = useState(''); - function closeModal() { + const closeModal = () => { setOpen(false); } @@ -24,7 +24,7 @@ const ManageAdminModal = ({ token, open, setOpen }: Props) => { * Endpoint to get all admins */ useEffect(() => { - async function getAdmins() { + const getAdmins = async () => { const response = await axios.post('/api/admin/users/get', { token: token }); @@ -40,7 +40,7 @@ const ManageAdminModal = ({ token, open, setOpen }: Props) => { * Removes an admin from the list, giving that user 'regular' privilege * @param user assumes that this user already has admin privilege */ - async function removeAdmin(user: Student) { + const removeAdmin = async (user: Student) => { const response = await axios.post('/api/admin/users/remove', { userId: user.netId, token: token @@ -58,7 +58,7 @@ const ManageAdminModal = ({ token, open, setOpen }: Props) => { * Calls endpoint to add or update a user with admin privilege * @param _netId the user's net id */ - async function addAdminByNetId(_netId: string) { + const addAdminByNetId = async (_netId: string) => { const response = await axios.post('/api/admin/users/add', { userId: _netId, token: token @@ -69,7 +69,7 @@ const ManageAdminModal = ({ token, open, setOpen }: Props) => { } } - function onTextChange(newText: string) { + const onTextChange = (newText: string) => { setNetId(newText); } diff --git a/client/src/modules/Admin/Components/Stats.tsx b/client/src/modules/Admin/Components/Stats.tsx index 6a8696ae..4dc62610 100644 --- a/client/src/modules/Admin/Components/Stats.tsx +++ b/client/src/modules/Admin/Components/Stats.tsx @@ -16,7 +16,7 @@ const Stats = ({ token }: StatsProps) => { Fires on every render to check for approvals or removals of reviews */ useEffect(() => { - async function getCounts() { + const getCounts = async () => { const response = await axios.post('/api/admin/reviews/count', { token: token }); @@ -34,7 +34,7 @@ const Stats = ({ token }: StatsProps) => { Function to download a file containing all reviewed classes in the database and their number of reviews */ - async function downloadCSVFile() { + const downloadCSVFile = async () => { const element = document.createElement('a'); let csv = ''; From 0e534da8d95ad25b85c3a3fc4426d84bbe128655 Mon Sep 17 00:00:00 2001 From: Simon Ilincev Date: Mon, 9 Dec 2024 19:04:17 -0500 Subject: [PATCH 5/6] style: make review summary gauge card responsive --- client/src/modules/Course/Styles/Gauges.module.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/modules/Course/Styles/Gauges.module.css b/client/src/modules/Course/Styles/Gauges.module.css index 318a30e3..f2262e8e 100644 --- a/client/src/modules/Course/Styles/Gauges.module.css +++ b/client/src/modules/Course/Styles/Gauges.module.css @@ -95,6 +95,7 @@ @media only screen and (max-width: 1216px) { .container { gap: 24px; + flex-direction: column; } .overallScore { @@ -128,6 +129,7 @@ .horizontal { gap: 6px; + flex-direction: column; } .ratingNum { @@ -135,6 +137,10 @@ font-weight: 590; } + .ratings { + height: auto; + } + .bars { display: flex; flex-flow: row nowrap; From f46586b69a66228cb5d156f4c7e7f58afab16ffd Mon Sep 17 00:00:00 2001 From: Andrew Qian Date: Tue, 10 Dec 2024 12:02:52 -0500 Subject: [PATCH 6/6] updating some breakpoints and alignments --- .../src/modules/Course/Components/Gauges.tsx | 28 +++++++++++-------- .../modules/Course/Styles/Gauges.module.css | 17 +++++++++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/client/src/modules/Course/Components/Gauges.tsx b/client/src/modules/Course/Components/Gauges.tsx index ab9f6c85..85e592de 100644 --- a/client/src/modules/Course/Components/Gauges.tsx +++ b/client/src/modules/Course/Components/Gauges.tsx @@ -157,12 +157,14 @@ const Gauges = ({overall, difficulty, workload}: GaugesProps) => { /> })}
-
{difficulty ? difficulty.toPrecision(2) : "-"}
- {difficultyHover} +
+
{difficulty ? difficulty.toPrecision(2) : "-"}
+ {difficultyHover} +
Workload
@@ -174,12 +176,14 @@ const Gauges = ({overall, difficulty, workload}: GaugesProps) => { /> })}
-
{workload ? workload.toPrecision(2) : "-"}
- {workloadHover} +
+
{workload ? workload.toPrecision(2) : "-"}
+ {workloadHover} +
diff --git a/client/src/modules/Course/Styles/Gauges.module.css b/client/src/modules/Course/Styles/Gauges.module.css index f2262e8e..b00066db 100644 --- a/client/src/modules/Course/Styles/Gauges.module.css +++ b/client/src/modules/Course/Styles/Gauges.module.css @@ -92,9 +92,15 @@ font-weight: 590; } -@media only screen and (max-width: 1216px) { +.responsiveLabel { + display: flex; + flex-direction: row; + gap: 14px; +} + +@media only screen and (max-width: 1124px) { .container { - gap: 24px; + gap: 12px; flex-direction: column; } @@ -139,6 +145,7 @@ .ratings { height: auto; + gap: 12px; } .bars { @@ -152,4 +159,10 @@ .bar { width: 30px; } + + .responsiveLabel { + display: flex; + flex-flow: row nowrap; + gap: 10px; + } } \ No newline at end of file