Skip to content

Commit 00dbcfb

Browse files
committed
groups and queues, creation, update, delete ok
1 parent 97c20b5 commit 00dbcfb

File tree

7 files changed

+951
-173
lines changed

7 files changed

+951
-173
lines changed

src/apps/competitions/admin.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from django import forms
12
from django.contrib import admin
23
from django.utils.translation import gettext_lazy as _
34
import json
45
import csv
56
from django.http import HttpResponse
6-
from profiles.models import User
7+
from profiles.models import CustomGroup, User
78
from . import models
9+
from django.contrib.auth.models import Group
10+
from django.contrib.admin.widgets import FilteredSelectMultiple
811

912

1013
# General class used to make custom filter
@@ -348,6 +351,46 @@ class PhaseExpansion(admin.ModelAdmin):
348351
]
349352

350353

354+
class CustomGroupAdminForm(forms.ModelForm):
355+
users = forms.ModelMultipleChoiceField(
356+
queryset=User.objects.all(),
357+
required=False,
358+
widget=FilteredSelectMultiple("Users", is_stacked=False),
359+
help_text="Add/Remove users for this group."
360+
)
361+
362+
class Meta:
363+
model = CustomGroup
364+
fields = ('name', 'permissions', 'queue', 'users')
365+
366+
def __init__(self, *args, **kwargs):
367+
super().__init__(*args, **kwargs)
368+
if self.instance and self.instance.pk:
369+
self.fields['users'].initial = self.instance.user_set.all()
370+
371+
372+
admin.site.unregister(Group)
373+
@admin.register(CustomGroup)
374+
class CustomGroupAdmin(admin.ModelAdmin):
375+
form = CustomGroupAdminForm
376+
list_display = ('name', 'queue')
377+
search_fields = ('name',)
378+
filter_horizontal = ('permissions',)
379+
fieldsets = (
380+
(None, {'fields': ('name',)}),
381+
('Permissions', {'fields': ('permissions',)}),
382+
('Utilisateurs', {'fields': ('users',)}),
383+
('Options', {'fields': ('queue',)}),
384+
)
385+
386+
def save_model(self, request, obj, form, change):
387+
super().save_model(request, obj, form, change)
388+
389+
def save_related(self, request, form, formsets, change):
390+
super().save_related(request, form, formsets, change)
391+
form.instance.user_set.set(form.cleaned_data['users'])
392+
393+
351394
admin.site.register(models.Competition, CompetitionExpansion)
352395
admin.site.register(
353396
models.CompetitionCreationTaskStatus, CompetitionCreationTaskStatusExpansion

src/apps/competitions/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from celery_config import app, app_for_vhost
1515
from leaderboards.models import SubmissionScore
16-
from profiles.models import User, Organization
16+
from profiles.models import CustomGroup, User, Organization
1717
from utils.data import PathWrapper
1818
from utils.storage import BundleStorage
1919
from PIL import Image
@@ -55,6 +55,15 @@ class Competition(models.Model):
5555
make_programs_available = models.BooleanField(default=False)
5656
make_input_data_available = models.BooleanField(default=False)
5757

58+
participant_groups = models.ManyToManyField(
59+
CustomGroup,
60+
blank=True,
61+
related_name='competitions',
62+
verbose_name="Groupes de participants",
63+
help_text="Competition owner being able to create groups of users."
64+
)
65+
66+
5867
queue = models.ForeignKey('queues.Queue', on_delete=models.SET_NULL, null=True, blank=True,
5968
related_name='competitions')
6069

src/apps/competitions/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@
1313
path('upload/', views.CompetitionUpload.as_view(), name="upload"),
1414
path('public/', views.CompetitionPublic.as_view(), name="public"),
1515
path('<int:pk>/detailed_results/<int:submission_id>/', views.CompetitionDetailedResults.as_view(), name="detailed_results"),
16+
17+
# Groups
18+
path('<int:pk>/groups/create/', views.competition_create_group, name='competition_create_group'),
19+
path('<int:pk>/groups/<int:group_id>/update/', views.competition_update_group),
20+
path('<int:pk>/groups/<int:group_id>/delete/', views.competition_delete_group),
21+
1622
]

src/apps/competitions/views.py

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
from django.views.generic import TemplateView, DetailView
44

55
from .models import Competition, CompetitionParticipant
6+
from django.core.serializers.json import DjangoJSONEncoder
7+
8+
from django.db.models import Q
9+
import json
10+
from django.contrib.auth.decorators import login_required
11+
from django.views.decorators.http import require_POST
12+
from django.shortcuts import get_object_or_404
13+
from django.http import JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect
14+
from django.urls import reverse
15+
from django.db import transaction
16+
from django.contrib import messages
17+
18+
19+
from profiles.models import CustomGroup, User
20+
from queues.models import Queue
21+
622

723

824
class CompetitionManagement(LoginRequiredMixin, TemplateView):
@@ -21,6 +37,47 @@ class CompetitionUpdateForm(LoginRequiredMixin, DetailView):
2137
template_name = 'competitions/form.html'
2238
queryset = Competition.objects.all()
2339

40+
41+
def get_context_data(self, **kwargs):
42+
ctx = super().get_context_data(**kwargs)
43+
comp = self.object
44+
45+
groups_qs = CustomGroup.objects.filter(
46+
Q(id__in=comp.participant_groups.values_list('id', flat=True))
47+
).select_related('queue').prefetch_related('user_set')
48+
49+
ctx['available_groups_json'] = json.dumps([
50+
{
51+
'id': g.id,
52+
'name': g.name,
53+
'queue': g.queue.name if g.queue else None,
54+
'members': [u.username for u in g.user_set.all()],
55+
}
56+
for g in groups_qs
57+
], cls=DjangoJSONEncoder)
58+
59+
ctx['selected_group_ids_json'] = json.dumps(
60+
list(comp.participant_groups.values_list('id', flat=True)),
61+
cls=DjangoJSONEncoder
62+
)
63+
64+
ctx['available_queues_json'] = json.dumps(
65+
list(Queue.objects.all().values('id', 'name')),
66+
cls=DjangoJSONEncoder
67+
)
68+
69+
ctx['available_users_json'] = json.dumps(
70+
list(
71+
User.objects
72+
.filter(is_active=True)
73+
.values('id', 'username', 'email')
74+
),
75+
cls=DjangoJSONEncoder
76+
)
77+
78+
return ctx
79+
80+
2481
def get_object(self, *args, **kwargs):
2582
competition = super().get_object(*args, **kwargs)
2683

@@ -76,7 +133,7 @@ def get_object(self, *args, **kwargs):
76133
# get participants from CompetitionParticipant where user=user and competition=competition
77134
is_participant = CompetitionParticipant.objects.filter(user=self.request.user, competition=competition).count() > 0
78135

79-
# check if secret key provided is valid
136+
# check if secret key provided is valid,
80137
valid_secret_key = self.request.GET.get('secret_key') == str(competition.secret_key)
81138

82139
if (
@@ -104,3 +161,157 @@ def get_context_data(self, **kwargs):
104161

105162
class CompetitionDetailedResults(LoginRequiredMixin, TemplateView):
106163
template_name = 'competitions/detailed_results.html'
164+
165+
166+
@login_required
167+
@require_POST
168+
def competition_create_group(request, pk):
169+
competition = get_object_or_404(Competition, pk=pk)
170+
171+
user = request.user
172+
if not (user.is_superuser or user == competition.created_by or user in competition.collaborators.all()):
173+
return HttpResponseForbidden("Not allowed")
174+
175+
if request.content_type == 'application/json':
176+
try:
177+
payload = json.loads(request.body.decode())
178+
except Exception:
179+
return HttpResponseBadRequest("Invalid JSON")
180+
name = (payload.get('name') or '').strip()
181+
queue_id = payload.get('queue_id')
182+
user_ids = payload.get('user_ids') or []
183+
else:
184+
name = (request.POST.get('name') or '').strip()
185+
queue_id = request.POST.get('queue_id') or None
186+
user_ids = request.POST.getlist('user_ids') or []
187+
if not user_ids and request.POST.get('user_ids'):
188+
user_ids = [u.strip() for u in request.POST.get('user_ids').split(',') if u.strip()]
189+
190+
if not name:
191+
return HttpResponseBadRequest("Missing name")
192+
193+
try:
194+
with transaction.atomic():
195+
group = CustomGroup(name=name)
196+
if queue_id:
197+
try:
198+
queue = Queue.objects.get(pk=queue_id)
199+
group.queue = queue
200+
except Queue.DoesNotExist:
201+
group.queue = None
202+
group.save()
203+
204+
if user_ids:
205+
# normalize to ints
206+
try:
207+
user_ids_int = [int(u) for u in user_ids]
208+
except Exception:
209+
user_ids_int = []
210+
if user_ids_int:
211+
users_qs = User.objects.filter(pk__in=user_ids_int)
212+
group.user_set.set(users_qs)
213+
214+
competition.participant_groups.add(group)
215+
216+
members = list(group.user_set.values_list('username', flat=True))
217+
group_data = {
218+
'id': group.id,
219+
'name': group.name,
220+
'queue': group.queue.name if group.queue else None,
221+
'members': members,
222+
}
223+
except Exception as e:
224+
return HttpResponseBadRequest("Error creating group: %s" % str(e))
225+
226+
if request.is_ajax() or request.content_type == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
227+
return JsonResponse({'status': 'ok', 'group': group_data})
228+
229+
messages.success(request, "Groupe créé")
230+
return HttpResponseRedirect(reverse('competitions:edit', kwargs={'pk': competition.pk}))
231+
232+
233+
@login_required
234+
@require_POST
235+
def competition_update_group(request, pk, group_id):
236+
competition = get_object_or_404(Competition, pk=pk)
237+
group = get_object_or_404(CustomGroup, pk=group_id)
238+
239+
user = request.user
240+
if not (user.is_superuser or user == competition.created_by or user in competition.collaborators.all()):
241+
return HttpResponseForbidden("Not allowed")
242+
243+
if request.content_type == 'application/json':
244+
try:
245+
payload = json.loads(request.body.decode())
246+
except Exception:
247+
return HttpResponseBadRequest("Invalid JSON")
248+
name = (payload.get('name') or '').strip()
249+
queue_id = payload.get('queue_id')
250+
user_ids = payload.get('user_ids', []) or []
251+
else:
252+
name = (request.POST.get('name') or '').strip()
253+
queue_id = request.POST.get('queue_id') or None
254+
user_ids = request.POST.getlist('user_ids[]') or []
255+
if not user_ids and request.POST.get('user_ids'):
256+
user_ids = [u.strip() for u in request.POST.get('user_ids').split(',') if u.strip()]
257+
258+
if not name:
259+
return HttpResponseBadRequest("Missing name")
260+
261+
try:
262+
with transaction.atomic():
263+
group.name = name
264+
if queue_id:
265+
group.queue = Queue.objects.filter(pk=queue_id).first()
266+
else:
267+
group.queue = None
268+
group.save()
269+
270+
# normalize user ids and set membership
271+
try:
272+
user_ids_int = [int(u) for u in user_ids]
273+
except Exception:
274+
user_ids_int = []
275+
group.user_set.set(User.objects.filter(pk__in=user_ids_int))
276+
except Exception as e:
277+
return HttpResponseBadRequest("Error updating group: %s" % str(e))
278+
279+
resp = {
280+
'status': 'ok',
281+
'group': {
282+
'id': group.id,
283+
'name': group.name,
284+
'queue': group.queue.name if group.queue else None,
285+
'members': list(group.user_set.values_list('username', flat=True)),
286+
}
287+
}
288+
289+
if request.is_ajax() or request.content_type == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
290+
return JsonResponse(resp)
291+
292+
messages.success(request, "Groupe modifié")
293+
return HttpResponseRedirect(reverse('competitions:edit', kwargs={'pk': competition.pk}))
294+
295+
296+
@login_required
297+
@require_POST
298+
def competition_delete_group(request, pk, group_id):
299+
competition = get_object_or_404(Competition, pk=pk)
300+
group = get_object_or_404(CustomGroup, pk=group_id)
301+
302+
user = request.user
303+
if not (user.is_superuser or user == competition.created_by or user in competition.collaborators.all()):
304+
return HttpResponseForbidden("Not allowed")
305+
306+
try:
307+
with transaction.atomic():
308+
competition.participant_groups.remove(group)
309+
group.delete()
310+
except Exception as e:
311+
return HttpResponseBadRequest("Error deleting group: %s" % str(e))
312+
313+
if request.is_ajax() or request.content_type == 'application/json' or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
314+
return JsonResponse({'status': 'ok', 'group_id': group_id})
315+
316+
messages.success(request, "Groupe supprimé")
317+
return HttpResponseRedirect(reverse('competitions:edit', kwargs={'pk': competition.pk}))

src/apps/profiles/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from queues.models import Queue
12
import uuid
23

34
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, UserManager
45
from django.db import models
6+
from django.contrib.auth.models import Group
57
from django.utils.timezone import now
68
from django.utils.text import slugify
79
from utils.data import PathWrapper
@@ -343,3 +345,22 @@ class Membership(models.Model):
343345

344346
class Meta:
345347
ordering = ["date_joined"]
348+
349+
350+
class CustomGroup(Group):
351+
352+
queue = models.ForeignKey(Queue,
353+
null=True,
354+
blank=True,
355+
on_delete=models.SET_NULL,
356+
related_name='custom_groups',
357+
verbose_name="Queue assignée au groupe",
358+
help_text="Queue à utiliser pour les utilisateurs membres de ce groupe (si définie)."
359+
)
360+
361+
class Meta:
362+
verbose_name = "Group"
363+
verbose_name_plural = "Groups"
364+
365+
def __str__(self):
366+
return self.name

0 commit comments

Comments
 (0)