Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save davidhcefx/9225435a182d6b8a2f6df99f501b28e7 to your computer and use it in GitHub Desktop.
Save davidhcefx/9225435a182d6b8a2f6df99f501b28e7 to your computer and use it in GitHub Desktop.
When using CTFd for in-class projects, you might want to display additional info such as "Is this user a course student?".

Make CTFd Display Custom Fields for In-class Projects

We are using CTFd for in-class projects. Here I share some modifications on CTFd for better in-class managements.

About Custom Fields

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.

ctfd set custom field

Therefore, each team now has a new attribute, which can be set to either true or false:

ctfd custom field view

The Problem

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:

ctfd custom field in scoreboard

Starting from Imitation

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".

Implementing a New One

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:

ctfd custom field in scoreboard

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment