Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/api/submissions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 5 additions & 8 deletions backend/src/api/submissions/putSubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,12 @@ export const putSubmissions = async (req: Request, res: Response): Promise<void>
// 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)

Expand Down
40 changes: 40 additions & 0 deletions backend/src/api/submissions/submissionsDoublyLinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class DoublyLinkedList<Submission extends RawSubmission>
{
if (this.tail)
{
io.emit('update_doubly_linked_list', { sid: this.tail.data.sid })

if (this.tail.prev)
{
this.tail = this.tail.prev
Expand Down Expand Up @@ -95,6 +97,43 @@ class DoublyLinkedList<Submission extends RawSubmission>
}
}

// 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_<Submission> | null
{
Expand All @@ -112,6 +151,7 @@ class DoublyLinkedList<Submission extends RawSubmission>
{
this.head = this.tail = null
this.size = 0
io.emit('update_doubly_linked_list')
}

// Function to check if the list is empty
Expand Down
1 change: 1 addition & 0 deletions backend/src/api/submissions/submissionsQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class SubmissionsQueue<Submission extends RawSubmission>
clear(): void
{
this.submissions = []
io.emit('update_queue')
}

// Checks if the specified submission is already in the queue
Expand Down
29 changes: 18 additions & 11 deletions backend/src/services/db/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,25 +87,32 @@ export default class MongoDB extends Database {

update(TableName: string, Key: Key, Item: Item): Promise<Item> {
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<string, any> = {}
const unsetFields: Record<string, any> = {}

// 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<string, any> = {}
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
}
)
})
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 43 additions & 2 deletions frontend/src/pages/admin/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Submission[]>()
// 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}` }
Expand All @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -98,12 +124,27 @@ const Home = (): React.JSX.Element => {
};
*/

// Show loading page until data is ready
if (isLoading) return <PageLoading />

// Main UI rendering
return (
<>
<Block size="xs-12">
<h1>Admin Dashboard</h1>
<div style={{ position: 'relative' }}>
<h1 style={{ marginBottom: 0 }}>Admin Dashboard</h1>
<Button
color="red"
content="Clear Queue"
onClick={ clearQueue }
style={{
position: 'absolute',
top: 0,
right: 0
}}
>
</Button>
</div>
</Block>

<Block size="xs-6">
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/pages/gold/Problem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProblemType>()
// Route param (problem ID from URL)
const { pid } = useParams<{ pid: string }>()

// Store the submissions list
const [submissions, setSubmissions] = useState<Submission[]>()
// 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]
Expand All @@ -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)
Expand All @@ -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`,
Expand Down Expand Up @@ -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 <Unauthorized />

// Show loading spinner while data is being fetched
if (isLoading) return <PageLoading />
// If problem data doesn't exist, show not found page
if (!problem) return <NotFound />

// Main component rendering
return (
<>
<Countdown />
Expand Down Expand Up @@ -108,7 +123,7 @@ const Problem = (): React.JSX.Element => {
{settings && new Date() < settings?.end_date ? (
<>
<Button
disabled={submissions?.filter(({ status, released }) => status == 'pending' || !released).length !== 0}
disabled={submissions?.filter(({ status, released }) => status == 'pending' || status === 'accepted' || !released).length !== 0}
as={Link}
to={`/gold/problems/${problem?.id}/submit`}
content="Submit"
Expand Down
Loading