Skip to content
Open
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
56 changes: 56 additions & 0 deletions apps/challenges/tests/test_queue_purge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse
from challenges.models import Challenge, ChallengePhase
from hosts.models import ChallengeHostTeam, ChallengeHost
from unittest.mock import patch

class QueuePurgeAPITestCase(APITestCase):

def setUp(self):
# Create a user and challenge host team
self.user = User.objects.create_user(username='hostuser', password='testpass')
self.host_team = ChallengeHostTeam.objects.create(team_name='Host Team', created_by=self.user)
self.challenge = Challenge.objects.create(title='Test Challenge', creator=self.host_team, published=True)
self.challenge_phase = ChallengePhase.objects.create(name='Phase 1', challenge=self.challenge)

# Make host relationship
self.challenge_host = ChallengeHost.objects.create(user=self.user, team_name=self.host_team, status=ChallengeHost.ACCEPTED)
self.url = reverse('purge_challenge_queue', kwargs={'challenge_pk': self.challenge.pk})

def test_purge_unauthenticated(self):
response = self.client.post(self.url, {'scope': 'all', 'dry_run': False})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_purge_forbidden_for_non_host(self):
non_host_user = User.objects.create_user(username='nonhost', password='pass')
self.client.force_authenticate(user=non_host_user)
response = self.client.post(self.url, {'scope': 'all', 'dry_run': False})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

@patch('challenges.services.QueuePurgeService.purge_queue')
def test_purge_success(self, mock_purge):
mock_purge.return_value = {
'purged': {'pending': 5, 'running': 2, 'total': 7},
'skipped': 0,
'took_ms': 123,
'notes': 'Purged successfully',
'queues': ['submission_1_1']
}
self.client.force_authenticate(user=self.user)
response = self.client.post(self.url, {'scope': 'all', 'dry_run': False})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('purged', response.data)
self.assertEqual(response.data['purged']['total'], 7)

def test_purge_invalid_scope(self):
self.client.force_authenticate(user=self.user)
response = self.client.post(self.url, {'scope': 'invalid-scope', 'dry_run': False})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_purge_challenge_not_found(self):
self.client.force_authenticate(user=self.user)
url = reverse('purge_challenge_queue', kwargs={'challenge_pk': 9999})
response = self.client.post(url, {'scope': 'all', 'dry_run': False})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
6 changes: 6 additions & 0 deletions apps/challenges/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.conf.urls import url
from django.urls import path
from .views import purge_challenge_queue

from . import views

Expand Down Expand Up @@ -314,6 +316,10 @@
views.modify_leaderboard_data,
name="modify_leaderboard_data",
),
url(
r"^challenge/(?P<challenge_pk>[0-9]+)/queue/purge/$",
views.purge_challenge_queue,
name="purge_challenge_queue"),
]

app_name = "challenges"
24 changes: 24 additions & 0 deletions apps/challenges/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
from datetime import datetime
from os.path import basename, isfile, join

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from challenges.models import Challenge
from hosts.utils import is_user_a_host_of_challenge
from .services import QueuePurgeService
from rest_framework import status

import pytz
import requests
import yaml
Expand Down Expand Up @@ -5043,6 +5051,22 @@ def update_challenge_attributes(request):
}
return Response(response_data, status=status.HTTP_200_OK)

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def purge_challenge_queue(request, challenge_pk):
if not is_user_a_host_of_challenge(request.user, challenge_pk):
return Response({'error': 'Not authorized.'}, status=status.HTTP_403_FORBIDDEN)
try:
challenge = Challenge.objects.get(pk=challenge_pk)
except Challenge.DoesNotExist:
return Response({'error': 'Challenge not found.'}, status=status.HTTP_404_NOT_FOUND)
scope = request.data.get('scope', 'all')
dry_run = request.data.get('dry_run', False)
purge_service = QueuePurgeService(challenge)
result = purge_service.purge_queue(scope=scope, dry_run=dry_run)
return Response(result)



@api_view(["PUT"])
@throttle_classes([UserRateThrottle])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ <h5 class="fw-light">My Submissions</h5>
</div>
</div>
<div class="ev-card-body collapsible-table">
<div class="row row-lr-margin" *ngIf="isChallengeHost">
<div class="col-sm-12 right-align">
<button class="btn btn-danger"
(click)="openPurgeModal()">
Purge Queue
</button>
</div>
</div>
<!-- Table to show the submissions -->
<table
mat-table
*ngIf="paginationDetails.showPagination == true && submissions.length > 0"
Expand Down Expand Up @@ -193,3 +202,24 @@ <h5 class="fw-light">My Submissions</h5>
</div>
</div>
</div>

<!-- Modal for purge queue -->
<!-- Purge Queue Confirmation Modal -->
<div *ngIf="showPurgeModal" class="modal-backdrop"></div>
<div *ngIf="showPurgeModal" class="modal">
<div class="modal-content">
<h5>Purge Submissions Queue</h5>
<p>This action will delete all pending submissions in the queue. <strong>This cannot be undone.</strong></p>
<label>
<input type="checkbox" [(ngModel)]="purgeConfirmed" />
I understand this action cannot be undone
</label>
<div class="modal-buttons">
<button class="btn btn-secondary" (click)="closePurgeModal()">Cancel</button>
<button class="btn btn-danger" [disabled]="!purgeConfirmed" (click)="confirmPurge()">
{{ purgeInProgress ? 'Purging...' : 'Confirm Purge' }}
</button>
</div>
</div>
</div>
<!-- End of Modal for purge queue -->
13 changes: 13 additions & 0 deletions frontend_v2/src/app/services/challenge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,17 @@ export class ChallengeService {
const API_PATH = this.endpointsService.challengeCreateURL(hostTeam);
return this.apiService.postFileUrl(API_PATH, formData);
}

/**
* Purge submissions queue for a challenge.
* @param challengeId - The challenge ID
* @param body - {scope: string, dry_run: boolean}
* @returns Observable<any>
*/
purgeQueue(challengeId: number, body: { scope: string; dry_run: boolean }) {
const apiPath = `/challenges/${challengeId}/queue/purge/`;
// Replace this.apiService.postUrl with your actual API POST call method
return this.apiService.postUrl(apiPath, JSON.stringify(body));
}

}