diff --git a/backend/src/api/submissions/index.ts b/backend/src/api/submissions/index.ts index 3f09de6c..a6453104 100644 --- a/backend/src/api/submissions/index.ts +++ b/backend/src/api/submissions/index.ts @@ -55,5 +55,17 @@ submissions.get('/submissions/submissionsDoublyLinkedList', isAuthenticated, (_r res.json(doublyLinkedList.get()) }) +// Route to remove submission from doubly linked list +submissions.post('/submissions/removeAtDoublyLinkedList', isAuthenticated, (req: Request, _res: Response) => { + const { sid } = req.body + doublyLinkedList.removeAt(sid) +}) + +// Route to clear both the queue and the doubly linked list +submissions.post('/submissions/clearQueueAndDoublyLinkedList', isAuthenticated, (_req: Request, _res: Response) => { + submissionsQueue.clear() + doublyLinkedList.clear() +}) + // Export the 'submissions' router to be used in the main app export default submissions diff --git a/backend/src/api/submissions/putSubmissions.ts b/backend/src/api/submissions/putSubmissions.ts index 7a8566d1..4b2d228f 100644 --- a/backend/src/api/submissions/putSubmissions.ts +++ b/backend/src/api/submissions/putSubmissions.ts @@ -180,15 +180,12 @@ export const putSubmissions = async (req: Request, res: Response): Promise // Get the existing submission using the submission ID (sid) const submission = await contest.get_submission(item.sid) - // Check if the 'claimed' field is being modified - if (item.claimed !== undefined && submission.claimed !== undefined) { - // Trying to change a claimed submission - if (req.user?.role !== 'admin' && item.claimed !== null) { - res.status(403).send({ message: 'This submission is already claimed!' }) - return - } + // Set claimed and claimed_date to null to remove those attributes from the submission data entry in the database + if (item.claimed === '' && submission.claimed !== undefined) { + item.claimed = null + item.claimed_date = null } - + // Update the submission with the new data provided in the request await contest.update_submission(item.sid, item) diff --git a/backend/src/api/submissions/submissionsDoublyLinkedList.ts b/backend/src/api/submissions/submissionsDoublyLinkedList.ts index e7e18cd0..f6ef74b5 100644 --- a/backend/src/api/submissions/submissionsDoublyLinkedList.ts +++ b/backend/src/api/submissions/submissionsDoublyLinkedList.ts @@ -62,6 +62,8 @@ class DoublyLinkedList { if (this.tail) { + io.emit('update_doubly_linked_list', { sid: this.tail.data.sid }) + if (this.tail.prev) { this.tail = this.tail.prev @@ -95,6 +97,43 @@ class DoublyLinkedList } } + // Function to remove node with specified sid + removeAt(sid: string): void + { + let currentNode = this.head + + while (currentNode) + { + if (currentNode.data.sid === sid) + { + if (currentNode === this.head) + { + this.removeFirst() + } + else if (currentNode === this.tail) + { + this.remove() + } + else + { + if (currentNode.prev) + { + currentNode.prev.next = currentNode.next + } + if (currentNode.next) + { + currentNode.next.prev = currentNode.prev + } + this.size-- + + io.emit('update_doubly_linked_list', { sid: sid }) + } + return + } + currentNode = currentNode.next + } + } + // Function to get a node at a specific index in the list getNodeAt(index: number): Node_ | null { @@ -112,6 +151,7 @@ class DoublyLinkedList { this.head = this.tail = null this.size = 0 + io.emit('update_doubly_linked_list') } // Function to check if the list is empty diff --git a/backend/src/api/submissions/submissionsQueue.ts b/backend/src/api/submissions/submissionsQueue.ts index 29be456e..7ece1ddc 100644 --- a/backend/src/api/submissions/submissionsQueue.ts +++ b/backend/src/api/submissions/submissionsQueue.ts @@ -85,6 +85,7 @@ export class SubmissionsQueue clear(): void { this.submissions = [] + io.emit('update_queue') } // Checks if the specified submission is already in the queue diff --git a/backend/src/services/db/mongo.ts b/backend/src/services/db/mongo.ts index f9803cb1..a6991db4 100644 --- a/backend/src/services/db/mongo.ts +++ b/backend/src/services/db/mongo.ts @@ -87,25 +87,32 @@ export default class MongoDB extends Database { update(TableName: string, Key: Key, Item: Item): Promise { return new Promise((resolve, reject) => { - const unsetFields = Object.assign( - {}, - ...Object.entries(Item) - .filter((obj) => obj[1] === undefined || obj[1] === null) - .map((obj) => ({ [`${obj[0]}`]: 1 })) - ) + const setFields: Record = {} + const unsetFields: Record = {} + + // Separate the fields into $set and $unset + for (const key in Item) { + if (Item[key] === null) { + unsetFields[key] = "" // Remove the field if it's null + } else { + setFields[key] = Item[key] // Update the field with the new value + } + } + + const updateOps: Record = {} + if (Object.keys(setFields).length) updateOps['$set'] = setFields + if (Object.keys(unsetFields).length) updateOps['$unset'] = unsetFields + // Perform the update this.db.collection(TableName).updateOne( Key, - { - $set: Item, - $unset: unsetFields - }, + updateOps, // Apply both $set and $unset as needed (err, data) => { if (err) { reject(err) return } - if (data) resolve(data as unknown as Item) + if (data) resolve(Item) // Return the original item as confirmation } ) }) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4f69e7b2..f2a8009e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,10 +17,10 @@ services: context: backend network: host environment: - - MONGO_HOST=mongo - - MONGO_USER=MUad - - MONGO_PASS=ep16y11BPqP - - MONGO_DBNAME=admin + - MONGO_HOST=mongodb + - MONGO_USER=username + - MONGO_PASS=password + - MONGO_DBNAME=abacus volumes: - "./backend/src:/app/src" ports: diff --git a/frontend/src/pages/admin/Home.tsx b/frontend/src/pages/admin/Home.tsx index eb1ec794..f220a138 100644 --- a/frontend/src/pages/admin/Home.tsx +++ b/frontend/src/pages/admin/Home.tsx @@ -4,20 +4,28 @@ import { AppContext, SocketContext } from 'context' import { Block, PageLoading } from 'components' import config from 'environment' import moment from 'moment' -import { Table } from 'semantic-ui-react' +import { Table, Button } from 'semantic-ui-react' import { Link } from 'react-router-dom' import { usePageTitle } from 'hooks' +// Main Admin Home Component const Home = (): React.JSX.Element => { + // Set the page title usePageTitle("Abacus | Admin") + // Using socket context for real-time updates const socket = useContext(SocketContext) + // Track loading state const [isLoading, setLoading] = useState(true) + // Store the submissions list const [submissions, setSubmissions] = useState() + // Track component mounted status const [isMounted, setMounted] = useState(true) + // Access the user and settings context to retrieve current user data and setting data const { user, settings } = useContext(AppContext) + // Function to load submissions from API and filter the submissions const loadSubmissions = async () => { const getSubmissions = await fetch(`${config.API_URL}/submissions`, { headers: { Authorization: `Bearer ${localStorage.accessToken}` } @@ -38,6 +46,22 @@ const Home = (): React.JSX.Element => { setLoading(false) } + // Function that clears the queue and doubly linked list + const clearQueue = async () => { + const response = await fetch(`${config.API_URL}/submissions/clearQueueAndDoublyLinkedList`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.accessToken}` + }, + }) + + if (response.ok) { + console.log("Queue has been cleared") + } + } + + // Effect hook to load submissions on component mount and set up socket listeners for real-time updates useEffect(() => { loadSubmissions().then(() => setLoading(false)) socket?.on('new_submission', loadSubmissions) @@ -46,8 +70,10 @@ const Home = (): React.JSX.Element => { return () => setMounted(false) }, []) + // Filter only flagged submissions const flaggedSubmissions = useMemo(() => submissions?.filter(({ flagged }) => flagged !== undefined), [submissions]) + // Generate time slot categories from start to end time const categories: string[] = [] if (settings?.start_date && settings?.end_date) { for (let time = Number(settings?.start_date); time <= Number(settings?.end_date); time += 1800000) { @@ -98,12 +124,27 @@ const Home = (): React.JSX.Element => { }; */ + // Show loading page until data is ready if (isLoading) return + // Main UI rendering return ( <> -

Admin Dashboard

+
+

Admin Dashboard

+ +
diff --git a/frontend/src/pages/gold/Problem.tsx b/frontend/src/pages/gold/Problem.tsx index 27dabd18..2a225979 100644 --- a/frontend/src/pages/gold/Problem.tsx +++ b/frontend/src/pages/gold/Problem.tsx @@ -9,13 +9,20 @@ import { AppContext } from 'context' import { userHome } from 'utils' import { usePageTitle } from 'hooks' +// Gold Problems Page Component const Problem = (): React.JSX.Element => { + // Access the user and settings context to retrieve current user data and setting data const { user, settings } = useContext(AppContext) + // Track loading state const [isLoading, setLoading] = useState(true) + // Store the problem const [problem, setProblem] = useState() + // Route param (problem ID from URL) const { pid } = useParams<{ pid: string }>() + // Store the submissions list const [submissions, setSubmissions] = useState() + // Get latest submission (if any) using useMemo to avoid unnecessary recalculations const latestSubmission = useMemo(() => { if (!submissions?.length || !user) return <> const { sid } = submissions[submissions.length - 1] @@ -26,10 +33,13 @@ const Problem = (): React.JSX.Element => { ) }, [submissions]) + // Track component mounted status const [isMounted, setMounted] = useState(true) + // Set the page title dynamically usePageTitle(`Abacus | ${problem?.name ?? ""}`) + // Effect hook to load problem on component mount useEffect(() => { loadProblem().then(() => { setLoading(false) @@ -39,6 +49,7 @@ const Problem = (): React.JSX.Element => { } }, []) + // Function to load problem data and user's submissions for this specific problem const loadProblem = async () => { let response = await fetch( `${config.API_URL}/problems?division=gold&id=${pid}&columns=description,project_id,design_document`, @@ -69,12 +80,16 @@ const Problem = (): React.JSX.Element => { } } + // If competition hasn't started and user isn't in the right division or role, block access if (!settings || new Date() < settings.start_date) if (user?.division != 'gold' && user?.role != 'admin') return + // Show loading spinner while data is being fetched if (isLoading) return + // If problem data doesn't exist, show not found page if (!problem) return + // Main component rendering return ( <> @@ -108,7 +123,7 @@ const Problem = (): React.JSX.Element => { {settings && new Date() < settings?.end_date ? ( <>