Skip to content

Commit 9650327

Browse files
committed
Add participant management without user authentication
Fixes #8 Add support for participants without user authentication and manage their participation in tournaments. * Add `Participant` class with a nullable foreign reference to `auth.User` in `tournaments/tournaments/models.py`. * Modify `Participation` class to reference `Participant` instead of `auth.User` in `tournaments/tournaments/models.py`. * Update `required_confirmations_count` method to count only participants with non-null `auth.User` in `tournaments/tournaments/models.py`. * Add `ManageParticipantsView` to manage participants without user authentication in `tournaments/frontend/views.py`. * Modify `JoinTournamentView` and `WithdrawTournamentView` to support participants without user authentication in `tournaments/frontend/views.py`. * Add migration to create `Participant` model and update `Participation` model in `tournaments/tournaments/migrations/0003_add_participant.py`. * Add HTML template for managing participants without user authentication in `tournaments/frontend/templates/frontend/manage-participants.html`. * Add tests to verify the new functionality for participants without user authentication in `tournaments/frontend/tests.py`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/kosmotive/tournaments/issues/8?shareId=XXXX-XXXX-XXXX-XXXX).
1 parent a806895 commit 9650327

File tree

5 files changed

+135
-11
lines changed

5 files changed

+135
-11
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% extends "frontend/base.html" %}
2+
3+
{% block title %}Manage Participants{% endblock %}
4+
5+
{% block header %}
6+
<h1>Manage Participants</h1>
7+
{% endblock %}
8+
9+
{% block content %}
10+
<form method="post" action="{% url 'manage-participants' pk=tournament.id %}">
11+
{% csrf_token %}
12+
<div class="form-group">
13+
<label for="participant_names">Participant Names (one per line)</label>
14+
<textarea class="form-control" id="participant_names" name="participant_names" rows="10" required></textarea>
15+
</div>
16+
<button type="submit" class="btn btn-primary">Save Participants</button>
17+
</form>
18+
{% endblock %}

tournaments/frontend/tests.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def add_participators(tournament, number = 10):
329329
tournament.save()
330330
users = [models.User.objects.get_or_create(username = f'user{idx}')[0] for idx in range(number)]
331331
for user in users:
332-
models.Participation.objects.create(tournament = tournament, user = user, slot_id = models.Participation.next_slot_id(tournament))
332+
models.Participation.objects.create(tournament = tournament, participant = models.Participant.objects.create(user = user, name = user.username), slot_id = models.Participation.next_slot_id(tournament))
333333
return users
334334

335335

@@ -500,7 +500,7 @@ def setUp(self):
500500

501501
for tournament in models.Participation.objects.all():
502502
for user in models.User.objects.all():
503-
models.Participation.objects.create(tournament = tournament, user = user, slot_id = models.Participation.next_slot_id(tournament))
503+
models.Participation.objects.create(tournament = tournament, participant = models.Participant.objects.create(user = user, name = user.username), slot_id = models.Participation.next_slot_id(tournament))
504504

505505
def test_unauthenticated(self):
506506
self.client.logout()
@@ -790,3 +790,37 @@ def test_post(self):
790790
self.assertContains(response, 'Your confirmation has been saved.')
791791
self.assertContains(response, 'Confirmations: 1 / 6')
792792
self.assertContains(response, 'You have confirmed.')
793+
794+
795+
class ManageParticipantsViewTests(TestCase):
796+
797+
def setUp(self):
798+
self.user1 = models.User.objects.create(username = 'test1')
799+
self.user2 = models.User.objects.create(username = 'test2')
800+
self.client.force_login(self.user1)
801+
802+
self.tournament1 = models.Tournament.load(definition = test_tournament1_yml, name = 'Test1', creator = self.user1, published = True)
803+
self.tournament2 = models.Tournament.load(definition = test_tournament1_yml, name = 'Test2', creator = self.user2, published = True)
804+
805+
def test_get(self):
806+
response = self.client.get(reverse('manage-participants', kwargs = dict(pk = self.tournament1.id)))
807+
self.assertEqual(response.status_code, 200)
808+
self.assertContains(response, 'Manage Participants')
809+
self.assertContains(response, 'Participant Names (one per line)')
810+
self.assertContains(response, 'Save Participants')
811+
812+
def test_post(self):
813+
participant_names = "Participant1\nParticipant2\nParticipant3"
814+
response = self.client.post(reverse('manage-participants', kwargs = dict(pk = self.tournament1.id)),
815+
dict(participant_names = participant_names), follow = True)
816+
self.assertEqual(response.status_code, 200)
817+
self.assertContains(response, 'Participants have been updated.')
818+
self.assertTrue(models.Participant.objects.filter(name = 'Participant1').exists())
819+
self.assertTrue(models.Participant.objects.filter(name = 'Participant2').exists())
820+
self.assertTrue(models.Participant.objects.filter(name = 'Participant3').exists())
821+
self.assertTrue(models.Participation.objects.filter(tournament = self.tournament1, participant__name = 'Participant1').exists())
822+
self.assertTrue(models.Participation.objects.filter(tournament = self.tournament1, participant__name = 'Participant2').exists())
823+
self.assertTrue(models.Participation.objects.filter(tournament = self.tournament1, participant__name = 'Participant3').exists())
824+
self.assertFalse(models.Participant.objects.filter(name = 'Participant1', participations__isnull = True).exists())
825+
self.assertFalse(models.Participant.objects.filter(name = 'Participant2', participations__isnull = True).exists())
826+
self.assertFalse(models.Participant.objects.filter(name = 'Participant3', participations__isnull = True).exists())

tournaments/frontend/views.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,11 @@ def get(self, request, *args, **kwargs):
226226
return HttpResponse(status = 412)
227227

228228
# Create the participation only if it does not already exist.
229-
if not self.object.participations.filter(user = request.user).exists():
229+
if not self.object.participations.filter(participant__user = request.user).exists():
230+
participant, created = models.Participant.objects.get_or_create(user = request.user, defaults = {'name': request.user.username})
230231
models.Participation.objects.create(
231232
tournament = self.object,
232-
user = self.request.user,
233+
participant = participant,
233234
slot_id = models.Participation.next_slot_id(self.object))
234235

235236
request.session['alert'] = dict(status = 'success', text = 'You have joined the tournament.')
@@ -248,13 +249,43 @@ def get(self, request, *args, **kwargs):
248249
return HttpResponse(status = 412)
249250

250251
# Delete the participation only if it exists.
251-
if self.object.participations.filter(user = request.user).exists():
252-
models.Participation.objects.filter(user = request.user).delete()
252+
if self.object.participations.filter(participant__user = request.user).exists():
253+
self.object.participations.filter(participant__user = request.user).delete()
253254

254255
request.session['alert'] = dict(status = 'success', text = 'You have withdrawn from the tournament.')
255256
return redirect('update-tournament', pk = self.object.id)
256257

257258

259+
class ManageParticipantsView(IsCreatorMixin, SingleObjectMixin, VersionInfoMixin, AlertMixin, View):
260+
261+
model = models.Tournament
262+
template_name = 'frontend/manage-participants.html'
263+
264+
def get(self, request, *args, **kwargs):
265+
self.object = self.get_object()
266+
context = self.get_context_data(**kwargs)
267+
context['participants'] = self.object.participations.all()
268+
return render(request, self.template_name, context)
269+
270+
def post(self, request, *args, **kwargs):
271+
self.object = self.get_object()
272+
participant_names = request.POST.get('participant_names')
273+
if participant_names:
274+
participant_names_list = participant_names.splitlines()
275+
for participant_name in participant_names_list:
276+
participant, created = models.Participant.objects.get_or_create(name = participant_name)
277+
if not self.object.participations.filter(participant = participant).exists():
278+
models.Participation.objects.create(
279+
tournament = self.object,
280+
participant = participant,
281+
slot_id = models.Participation.next_slot_id(self.object)
282+
)
283+
# Automatically delete participants that are not part of any tournament
284+
models.Participant.objects.filter(participations__isnull = True, user__isnull = True).delete()
285+
request.session['alert'] = dict(status = 'success', text = f'Participants have been updated.')
286+
return redirect('manage-participants', pk = self.object.id)
287+
288+
258289
class TournamentProgressView(SingleObjectMixin, VersionInfoMixin, AlertMixin, View):
259290

260291
model = models.Tournament
@@ -299,7 +330,7 @@ def get_level_data(self, stage, level):
299330
def get_fixture_data(self, stage, level, fixture):
300331
return {
301332
'data': fixture,
302-
'editable': not fixture.is_confirmed and level == stage.current_level and self.request.user.id and stage.tournament.participations.filter(user = self.request.user).count() > 0,
333+
'editable': not fixture.is_confirmed and level == stage.current_level and self.request.user.id and stage.tournament.participations.filter(participant__user = self.request.user).count() > 0,
303334
'has_confirmed': fixture.confirmations.filter(id = self.request.user.id).count() > 0,
304335
}
305336

@@ -330,7 +361,7 @@ def post(self, request, *args, **kwargs):
330361
fixture = models.Fixture.objects.get(id = request.POST.get('fixture_id'))
331362

332363
# Check whether the user is a participator.
333-
if not request.user.id or self.object.participations.filter(user = request.user).count() == 0:
364+
if not request.user.id or self.object.participations.filter(participant__user = request.user).count() == 0:
334365
return HttpResponseForbidden()
335366

336367
# Check whether the tournament is active.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.db import migrations, models
2+
import django.db.models.deletion
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
('tournaments', '0002_remove_fixture_position_fixture_extras'),
8+
]
9+
10+
operations = [
11+
migrations.CreateModel(
12+
name='Participant',
13+
fields=[
14+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
15+
('name', models.CharField(max_length=100)),
16+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participants', to='auth.User')),
17+
],
18+
),
19+
migrations.AddField(
20+
model_name='participation',
21+
name='participant',
22+
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='tournaments.Participant'),
23+
preserve_default=False,
24+
),
25+
migrations.AlterUniqueTogether(
26+
name='participation',
27+
unique_together={('tournament', 'slot_id'), ('tournament', 'participant'), ('tournament', 'podium_position')},
28+
),
29+
migrations.RemoveField(
30+
model_name='participation',
31+
name='user',
32+
),
33+
]

tournaments/tournaments/models.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,17 @@ def delete_tournament_stages(sender, instance, **kwargs):
199199
instance.stages.non_polymorphic().all().delete()
200200

201201

202+
class Participant(models.Model):
203+
user = models.ForeignKey('auth.User', on_delete = models.SET_NULL, related_name = 'participant', null = True, blank = True)
204+
name = models.CharField(max_length = 100)
205+
206+
def __str__(self):
207+
return self.name
208+
209+
202210
class Participation(models.Model):
203211

204-
user = models.ForeignKey('auth.User', on_delete = models.PROTECT, related_name = 'participations')
212+
participant = models.ForeignKey('Participant', on_delete = models.CASCADE, related_name='participations')
205213
tournament = models.ForeignKey('Tournament', on_delete = models.CASCADE, related_name = 'participations')
206214
slot_id = models.PositiveIntegerField()
207215
podium_position = models.PositiveIntegerField(null = True, blank = True)
@@ -214,7 +222,7 @@ class Meta:
214222
ordering = ('tournament', 'slot_id')
215223
unique_together = [
216224
('tournament', 'slot_id'),
217-
('tournament', 'user'),
225+
('tournament', 'participant'),
218226
('tournament', 'podium_position'),
219227
]
220228

@@ -846,7 +854,7 @@ def players(self):
846854

847855
@property
848856
def required_confirmations_count(self):
849-
return 1 + self.mode.tournament.participations.count() // 2
857+
return 1 + self.mode.tournament.participations.filter(participant__user__isnull = False).count() // 2
850858

851859
@property
852860
def is_confirmed(self):

0 commit comments

Comments
 (0)