We are using CTFd for in-class projects. Here I share some modifications on CTFd for better in-class managements.
First of all, in the Admin Panel, there's an option for adding Custom Fields to either all Users or Teams.
For instance, in the screenshot below I added a field called "CourseStudent" to each Teams.
Therefore, each team now has a new attribute, which can be set to either true or false:
We don't want to impose restrictions on who can register CTFd, because we think it would be great for others to enjoy the challenges. On the other hand, in order to effectively keep track of students' progress, it is important to know which teams are the course students and which are not. Although we could still find that out by visiting each of the Team pages individually, it is a cumbersome task!
My idea is to integrate the IsCourseStudent
info into the Scoreboard page, like so:
First of all, notice that there are similar code in /themes/admin/templates/teams/team.html
:
# https://github.com/CTFd/CTFd/blob/cb5ba26bdb68fa4b40e112d54bc9cc5b3e51f546/CTFd/themes/admin/templates/teams/team.html#L173
{% for field in team.get_fields(admin=true) %}
<h3 class="d-block">
{{ field.name }}: {{ field.value }}
</h3>
{% endfor %}
which is the part responsible of displaying "CourseStudent: True" within the second screenshot.
At first, one might be tempted to reuse the above method get_fields()
directly on each Teams
class. But I wonder where did those team
variables came from?
# https://github.com/CTFd/CTFd/blob/cb5ba26bdb68fa4b40e112d54bc9cc5b3e51f546/CTFd/admin/teams.py#L47
@admin.route("/admin/teams/<int:team_id>")
@admins_only
def teams_detail(team_id):
team = Teams.query.filter_by(id=team_id).first_or_404()
...
return render_template(
"admin/teams/team.html",
team=team,
...
)
Okay, now you know that each team
variable came from a Teams.query
, which really is a database query!
Therefore, for N registered teams we have to first 1) get each team
variable, then 2) call team.get_fields()
on them. Part (1) alone would generate N additional requests, which is pretty bad. Note that the exact same goal can be achieved by a single query:
-- USE ctfd;
SELECT team_id, value FROM field_entries WHERE field_id = 2;
assuming field_id
2 corresponds to "CourseStudent".
Before modifying the HTML template, we have to first provide the relevant data from table field_entries
in database ctfd
. Therefore, I created a query object in /admin/scoreboard.py
based on the above SQL command:
@@ -2,11 +2,26 @@ from flask import render_template
from CTFd.admin import admin
from CTFd.scoreboard import get_standings
+from CTFd.models import db
+from CTFd.models import TeamFieldEntries as Entries
from CTFd.utils.decorators import admins_only
@admin.route("/admin/scoreboard")
@admins_only
def scoreboard_listing():
+ # get a list of student teams (field_id 2 == CourseStudent)
+ team_list = (
+ db.session.query(
+ Entries.team_id
+ )
+ .filter(Entries.field_id == 2, Entries.value == 'true')
+ )
+ student_teams = [team.team_id for team in team_list]
standings = get_standings(admin=True)
- return render_template("admin/scoreboard.html", standings=standings)
+
+ return render_template(
+ "admin/scoreboard.html",
+ standings=standings,
+ student_teams=student_teams,
+ )
The above query is equivalent to SELECT team_id FROM field_entries WHERE field_id = 2 AND value = 'true'
. There is a for-loop right after the query, because I want a list of integers rather than a list of sqlalchemy.result
. For your information, there is also a UserFieldEntries
class besides TeamFieldEntries
.
Finally, its time to modify the HTML template!
What we want to add to is the scoreboard.html inside of the Admin Panel. Here is what I've added:
@@ -32,7 +32,8 @@
<th class="sort-col"><b>Team</b></th>
<th class="sort-col"><b>Score</b></th>
<th class="sort-col"><b>Visibility</b></th>
+ <th class="sort-col"><b>CourseStudent</b></th>
</tr>
</thead>
<tbody>
{% for standing in standings %}
@@ -67,6 +68,13 @@
<span class="badge badge-success">visible</span>
{% endif %}
</td>
+ <td>
+ {% if standing.account_id in student_teams %}
+ <span class="badge badge-success">true</span>
+ {% else %}
+ <span class="badge badge-danger">false</span>
+ {% endif %}
+ </td>
</tr>
{% endfor %}
</tbody>
The first part is for adding a new column to display the words "CourseStudent". As you can see, there is a big for-loop iterating through each standings
. Since standing.accout_id
here is the same as each team's team_id
, we can just check if team_id
is in our list of integers to see if the team is "CourseStudent" or not.
The final result: