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
11 changes: 10 additions & 1 deletion ctef_core/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from django.contrib import admin
from .models import TaskModule, Task, TaskAttempt, TaskClue
from .models import (
TaskModule,
Task,
TaskAttempt,
TaskClue,
Competition,
CompetitionParticipation,
)

# Register your models here.
admin.site.register(TaskModule)
admin.site.register(Task)
admin.site.register(TaskAttempt)
admin.site.register(TaskClue)
admin.site.register(Competition)
admin.site.register(CompetitionParticipation)
36 changes: 28 additions & 8 deletions ctef_core/common.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from django.apps import apps
from django.http import HttpRequest, HttpResponseBadRequest
from django.contrib.auth.models import User

from ctef_core.models import Task, TaskAttempt
from ctef_core.models import Task, TaskAttempt, Competition, CompetitionParticipation
from ctef_web.forms import SecretForm


def get_module_key(module):
return module.split(".")[0]


def get_default_competition():
default_competition = Competition.objects.get(id=0)
return default_competition


def fetch_ctf_modules(additional_flag=None):
"""Fetches all django apps with modules installed within them."""

Expand Down Expand Up @@ -49,14 +53,23 @@ def get_base_context(request, include_tasks=True):


def validate_secret(attempt: TaskAttempt, secret: str) -> bool:
if attempt.task.secret == secret:
attempt.passed = True
attempt.save()

return True
else:
if attempt.task.secret != secret:
return False

attempt.passed = True
attempt.save()

default_competition = get_default_competition()

participation, _ = CompetitionParticipation.objects.get_or_create(
user=attempt.user, competition=default_competition
)
participation.score += 1
participation.save()

return True


def task_view_wrapper(view, slug):
def wrapper(request: HttpRequest):
Expand All @@ -67,9 +80,16 @@ def wrapper(request: HttpRequest):
task = Task.objects.get(slug=slug)
context["task"] = task

# Get the default competition
default_competition = get_default_competition()

attempt, _ = TaskAttempt.objects.get_or_create(
task=task, user=user, defaults={"points": task.points}
task=task,
user=user,
defaults={"points": task.points},
competition=default_competition,
)

context["task_attempt"] = attempt

if request.method == "POST":
Expand Down
59 changes: 59 additions & 0 deletions ctef_core/migrations/0010_competition_competitionparticipation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 5.0.11 on 2025-04-15 15:36

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ctef_core", "0009_task_desc"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Competition",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=500, unique=True)),
],
),
migrations.CreateModel(
name="CompetitionParticipation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"competition",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="ctef_core.competition",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
22 changes: 22 additions & 0 deletions ctef_core/migrations/0011_default_competition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.0.11 on 2025-04-15 15:25

from django.db import migrations, transaction


def add_default_competition(apps, schema_editor):
Competition = apps.get_model("ctef_core.Competition")

with transaction.atomic():
default_competition, _ = Competition.objects.get_or_create(
name="Default Competition", id=0
)
default_competition.save()


class Migration(migrations.Migration):

dependencies = [
("ctef_core", "0010_competition_competitionparticipation"),
]

operations = [migrations.RunPython(add_default_competition)]
25 changes: 25 additions & 0 deletions ctef_core/migrations/0012_taskattempt_competition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.0.11 on 2025-04-15 15:47

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ctef_core", "0011_default_competition"),
]

operations = [
migrations.AddField(
model_name="taskattempt",
name="competition",
field=models.ForeignKey(
default=0,
null=None,
on_delete=django.db.models.deletion.CASCADE,
to="ctef_core.competition",
),
preserve_default=False,
),
]
18 changes: 18 additions & 0 deletions ctef_core/migrations/0013_competitionparticipation_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.11 on 2025-04-15 15:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ctef_core", "0012_taskattempt_competition"),
]

operations = [
migrations.AddField(
model_name="competitionparticipation",
name="score",
field=models.PositiveSmallIntegerField(default=0),
),
]
17 changes: 17 additions & 0 deletions ctef_core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@


# Create your models here.
class Competition(models.Model):
name = models.CharField(max_length=500, unique=True, null=False)

def __str__(self):
return f'Competition "{self.name}"'


class CompetitionParticipation(models.Model):
score = models.PositiveSmallIntegerField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)

def __str__(self):
return f'{self.user.username}\'s participation in competition "{self.competition.name}"'


class TaskModule(models.Model):
"""A grouping of tasks with a common theme or technique."""

Expand Down Expand Up @@ -55,6 +71,7 @@ class TaskAttempt(models.Model):
passed = models.BooleanField(default=False)
task = models.ForeignKey(Task, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE, null=None)

def __str__(self) -> str:
return f"{self.user.username}'s attempt on task '{self.task.name}'"
6 changes: 6 additions & 0 deletions ctef_web/templates/ctef_web/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@

<p>Leaderboard!</p>

<ol>
{% for participant in participants %}
<li>{{participant.user.username}} ({{participant.score}} points)</li>
{% endfor %}
</ol>

{% endblock %}
12 changes: 10 additions & 2 deletions ctef_web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from django.contrib.auth.models import User
from django.contrib.auth import login, logout as logout_user

from ctef_core.common import get_base_context
from ctef_core.common import get_base_context, get_default_competition
from ctef_core.decorators import TaskDesc
from ctef_core.models import Task, TaskAttempt, TaskClue
from ctef_core.models import Task, TaskAttempt, TaskClue, CompetitionParticipation

from .forms import EnterForm

Expand All @@ -25,6 +25,14 @@ def tasks(request: HttpRequest):

def leaderboard(request: HttpRequest):
context = get_base_context(request, False)

default_competition = get_default_competition()
participants = CompetitionParticipation.objects.filter(
competition=default_competition
).order_by("score")

context["participants"] = participants

return render(request, "ctef_web/leaderboard.html", context)


Expand Down
1 change: 0 additions & 1 deletion static/shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ body {
}

.content {
grid-column: main;
grid-row: main;

padding-top: 1%;
Expand Down
4 changes: 3 additions & 1 deletion templates/shell.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ <h2 class="accordion-header">
</div>
{% endif %}

<div class="content bg-body-primary container-fluid">
<!-- Rescale the content size depending on if the side bar is present or not -->
<div class="content bg-body-primary container-fluid"
style="grid-column-end: end; grid-column-start: {% if module_tasks %} main {% else %} side {% endif %};">
{% block content %}{% endblock %}
</div>

Expand Down