diff --git a/.docker/init-mongo.js b/.docker/init-mongo.js index 124a2356..77f0b8cc 100644 --- a/.docker/init-mongo.js +++ b/.docker/init-mongo.js @@ -1,8 +1,8 @@ db.getSiblingDB('abacus') db.createUser({ - user: "username", - pwd: "password", + user: "MUad", + pwd: "ep16y11BPqP", roles: [{ role: "readWrite", db: "abacus" @@ -695,6 +695,7 @@ db.setting.insert({ points_per_minute: "1" }) +db.createCollection('standing') db.createCollection('problem') diff --git a/.github/workflows/ci-test-backend.yml b/.github/workflows/ci-test-backend.yml index d30681c2..51156669 100644 --- a/.github/workflows/ci-test-backend.yml +++ b/.github/workflows/ci-test-backend.yml @@ -15,8 +15,11 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '16' - - uses: egordm/gha-yarn-node-cache@v1 + node-version: '18' + - uses: actions/cache@v4 + with: + path: backend/package.json + key: npm-${{ hashFiles('package-lock.json') }} - run: yarn install working-directory: backend - run: yarn build diff --git a/.github/workflows/ci-test-frontend.yml b/.github/workflows/ci-test-frontend.yml index c6140e78..e262b459 100644 --- a/.github/workflows/ci-test-frontend.yml +++ b/.github/workflows/ci-test-frontend.yml @@ -15,8 +15,11 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '16' - - uses: egordm/gha-yarn-node-cache@v1 + node-version: '18' + - uses: actions/cache@v4 + with: + path: backend/package.json + key: npm-${{ hashFiles('package-lock.json') }} - run: yarn install working-directory: frontend - run: yarn build diff --git a/backend/Dockerfile b/backend/Dockerfile index 5693c89b..0eac8688 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # pull official base image -FROM node:16 +FROM node:18 ENV CHOKIDAR_USEPOLLING true diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod index ef439ab2..b84d0f90 100644 --- a/backend/Dockerfile.prod +++ b/backend/Dockerfile.prod @@ -1,5 +1,5 @@ # pull official base image -FROM node:16 as build +FROM node:18 as build # set working directory WORKDIR /app diff --git a/backend/package.json b/backend/package.json index 8002631e..55531ba6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,6 +47,7 @@ "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", "@types/uuid": "^8.3.0", + "@actions/cache": "^4.0.0", "nodemon": "^2.0.22", "openapi-types": "^12.1.3", "ts-node": "10.9.1", diff --git a/backend/src/@types/abacus/index.d.ts b/backend/src/@types/abacus/index.d.ts index 69c05f6f..11efd023 100644 --- a/backend/src/@types/abacus/index.d.ts +++ b/backend/src/@types/abacus/index.d.ts @@ -386,6 +386,13 @@ declare module 'abacus' { type?: 'success' | 'warning' | 'error' } + export interface Standing extends Record { + division: string + problems: Problem[] + standings: any + time_updated: number + } + export type Item = Record export type Args = Record } diff --git a/backend/src/abacus/contest.ts b/backend/src/abacus/contest.ts index 134b47bc..307999e9 100644 --- a/backend/src/abacus/contest.ts +++ b/backend/src/abacus/contest.ts @@ -1,4 +1,4 @@ -import { Args, Clarification, Item, Problem, ResolvedSubmission, Settings, Submission, User } from 'abacus' +import { Args, Clarification, Item, Problem, ResolvedSubmission, Settings, Submission, User, Standing } from 'abacus' import { Lambda } from 'aws-sdk' import { Database } from '../services' import { MongoDB } from '../services/db' @@ -142,6 +142,20 @@ class ContestService { save_settings(settings: Record): Promise { return this.db.update('setting', {}, settings) as Promise } + + /* Standings */ + + async create_standing(item: Item): Promise { + return this.db.put('standing', item) as Promise + } + + async get_standing(division: string): Promise { + return this.db.get('standing', {division}) as Promise + } + + async update_standing(division: string, item: Item): Promise { + return this.db.update('standing', {division}, item) as Promise + } } export default new ContestService() diff --git a/backend/src/api/standings/getStandings.ts b/backend/src/api/standings/getStandings.ts index 9ceafaa0..82cbfb12 100644 --- a/backend/src/api/standings/getStandings.ts +++ b/backend/src/api/standings/getStandings.ts @@ -103,7 +103,8 @@ interface BlueTeam { > } -const getBlueStandings = async (isPractice: boolean): Promise> => { +// Function that calculates blue standings and stores the standings in the database +const calculateBlueStandings = async (isPractice: boolean): Promise> => { let teams = await contest.get_users({ role: 'team', division: 'blue' }) teams = teams.filter((user) => !user.disabled) @@ -191,12 +192,54 @@ const getBlueStandings = async (isPractice: boolean): Promise => { + const current_time = Date.now() + + if (current_time < (time_updated + (5 * 60 * 1000))) { + return false + } + else + return true +} + +// Function that returns the blue standings +const getBlueStandings = async (isPractice: boolean): Promise> => { + const standing = await contest.get_standing('blue') + + if (await isTimeToUpdateStandings(standing.time_updated) === false) { + return { + problems: Object.values(standing.problems), + standings: standing.standings + } + } + else { + const standing = calculateBlueStandings(isPractice) + return { + problems: Object.values((await standing).problems), + standings: (await standing).standings + } + } +} + interface GoldTeam { display_name: string uid: string @@ -205,7 +248,8 @@ interface GoldTeam { problems: Record } -const getGoldStandings = async (isPractice: boolean): Promise> => { +// Function that calculates gold standings and stores the standings in the database +const calculateGoldStandings = async (isPractice: boolean): Promise> => { let teams = Object.values(await contest.get_users({ role: 'team', division: 'gold' })) teams = teams.filter((user) => !user.disabled) @@ -286,12 +330,45 @@ const getGoldStandings = async (isPractice: boolean): Promise> => { + const standing = await contest.get_standing('gold') + + if (await isTimeToUpdateStandings(standing.time_updated) === false) { + return { + problems: Object.values(standing.problems), + standings: standing.standings + } + } + else { + const standing = calculateGoldStandings(isPractice) + + return { + problems: Object.values((await standing).problems), + standings: (await standing).standings + } + } +} + +// Function that returns either blue or gold standings export const getStandings = async (req: Request, res: Response): Promise => { const errors = validationResult(req).array() if (errors.length > 0) { diff --git a/backend/src/api/submissions/index.ts b/backend/src/api/submissions/index.ts index e2df9b81..3f09de6c 100644 --- a/backend/src/api/submissions/index.ts +++ b/backend/src/api/submissions/index.ts @@ -7,6 +7,7 @@ import { postSubmissions, schema as postSchema } from './postSubmissions' import { putSubmissions, schema as putSchema } from './putSubmissions' import { rerunSubmission, schema as rerunSchema } from './rerunSubmission' import { submissionsQueue } from './submissionsQueue' +import { doublyLinkedList } from './submissionsDoublyLinkedList' /** * @swagger @@ -49,5 +50,10 @@ submissions.post('/submissions/submissionsEnqueue', isAuthenticated, (req: Reque submissionsQueue.enqueue(submission) }) +// Route to get the current state of the doubly linked list +submissions.get('/submissions/submissionsDoublyLinkedList', isAuthenticated, (_req: Request, res: Response) => { + res.json(doublyLinkedList.get()) +}) + // Export the 'submissions' router to be used in the main app export default submissions diff --git a/backend/src/api/submissions/rerunSubmission.ts b/backend/src/api/submissions/rerunSubmission.ts index 7a511f1c..c66ee8ea 100644 --- a/backend/src/api/submissions/rerunSubmission.ts +++ b/backend/src/api/submissions/rerunSubmission.ts @@ -2,6 +2,7 @@ import axios from 'axios' import { Request, Response } from 'express' import { matchedData, ParamSchema, validationResult } from 'express-validator' import { contest } from '../../abacus' +import { io } from '../../server' // Define the validation schema for the request body export const schema: Record = { @@ -152,6 +153,9 @@ export const rerunSubmission = async (req: Request, res: Response): Promise @@ -35,6 +36,8 @@ class DoublyLinkedList this.head = this.tail = newNode } this.size++ + + io.emit('update_doubly_linked_list', { sid: data.sid }) } // Function to prepend a new node with submission data to the beginning of the list @@ -77,6 +80,8 @@ class DoublyLinkedList { if (this.head) { + io.emit('update_doubly_linked_list', { sid: this.head.data.sid }) + if (this.head.next) { this.head = this.head.next @@ -125,6 +130,19 @@ class DoublyLinkedList } console.log(result.slice(0, -4)) } + + // Returns the current list of submission in the doubly linked list + get(): Submission[] { + let currentNode = this.head + const doublyLinkedList: Submission[] = [] + + while (currentNode) { + doublyLinkedList.push(currentNode.data) + currentNode = currentNode.next + } + + return doublyLinkedList + } } // Create an instance of DoublyLinkedList diff --git a/backend/src/api/submissions/submissionsQueue.ts b/backend/src/api/submissions/submissionsQueue.ts index 2dd0f911..29be456e 100644 --- a/backend/src/api/submissions/submissionsQueue.ts +++ b/backend/src/api/submissions/submissionsQueue.ts @@ -1,6 +1,6 @@ import { RawSubmission, Submission, User } from "abacus" import { doublyLinkedList } from "./submissionsDoublyLinkedList" -import { sendNotification } from '../../server' +import { io, sendNotification } from '../../server' // Class that represents the submissions queue export class SubmissionsQueue @@ -29,6 +29,7 @@ export class SubmissionsQueue return } this.submissions.push(item) + io.emit('update_queue', { sid: item.sid }) } /* Removes a submission for the queue by its submission ID (sid). @@ -40,13 +41,14 @@ export class SubmissionsQueue if (index !== -1) { + io.emit('update_queue', { sid: this.submissions[index].sid }) this.submissions.splice(index, 1) } if (!doublyLinkedList.isEmpty()) { const submission = doublyLinkedList.getNodeAt(0)?.data as Submission - this.submissions.push(submission) + this.enqueue(submission) doublyLinkedList.removeFirst() const judgeInfo = submission.claimed as User sendNotification({ diff --git a/frontend/src/context/socket.ts b/frontend/src/context/socket.ts index 3df8d447..c1c30be3 100644 --- a/frontend/src/context/socket.ts +++ b/frontend/src/context/socket.ts @@ -8,6 +8,8 @@ interface ClientToServerEvents { new_clarification: () => void; update_submission: (submission: Submission) => void delete_submission: (submission: Submission) => void + update_queue: (submission: Submission) => void + update_doubly_linked_list: (submission: Submission) => void } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/frontend/src/pages/admin/submissions/Submission.tsx b/frontend/src/pages/admin/submissions/Submission.tsx index 40ea131f..6e0c6e96 100644 --- a/frontend/src/pages/admin/submissions/Submission.tsx +++ b/frontend/src/pages/admin/submissions/Submission.tsx @@ -4,15 +4,17 @@ import { useNavigate, useParams } from 'react-router-dom' import { NotFound, PageLoading, SubmissionView } from 'components' import config from 'environment' import { Button, Grid } from 'semantic-ui-react' -import { AppContext } from 'context' +import { AppContext, SocketContext } from 'context' import { saveAs } from 'file-saver' import { usePageTitle } from 'hooks' -// unctional component for viewing and interacting with a specific submission +// Functional component for viewing and interacting with a specific submission const Submission = (): React.JSX.Element => { // Set page title usePageTitle("Abacus | Admin Submission") + // Contexts to get socket connection and app-wide state + const socket = useContext(SocketContext) // Get the submission ID from URL params const { sid } = useParams<{ sid: string }>() // State to hold submission details @@ -79,6 +81,10 @@ const Submission = (): React.JSX.Element => { loadSubmission() // Load the queue data loadQueue() + // Listen for 'update_submission' events from socket + socket?.on('update_submission', loadSubmission) + // Listen for 'update_queue' events from socket + socket?.on('update_queue', loadQueue) return () => { setMounted(false) } @@ -258,14 +264,23 @@ const Submission = (): React.JSX.Element => { return (