Skip to content

Commit

Permalink
Bring back the hacker profiles (pwncollege#303)
Browse files Browse the repository at this point in the history
* add a fast(er) global rankings function

* restore a reasonably fast user profile

* restore the hacker profile, and add badges

* disable profiles for hidden users
  • Loading branch information
zardus authored and mudongliang committed Mar 1, 2024
1 parent 540177d commit 4893f54
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 41 deletions.
16 changes: 2 additions & 14 deletions dojo_plugin/api/v1/scoreboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ...models import Dojos, DojoChallenges, DojoUsers, DojoMembers, DojoAdmins, DojoStudents, DojoModules, DojoChallengeVisibilities, Belts, Emojis
from ...utils import dojo_standings, user_dojos, first_bloods, daily_solve_counts
from ...utils.dojo import dojo_route, dojo_accessible
from ...utils.awards import get_belts, belt_asset
from ...utils.awards import get_belts, belt_asset, get_viewable_emojis

SCOREBOARD_CACHE_TIMEOUT_SECONDS = 60 * 60 * 2 # two hours make to cache all scoreboards
scoreboard_namespace = Namespace("scoreboard")
Expand Down Expand Up @@ -99,19 +99,7 @@ def get_scoreboard_page(model, duration=None, page=1, per_page=20):
end_idx = start_idx + per_page
pagination = Pagination(None, page, per_page, len(results), results[start_idx:end_idx])
user = get_current_user()

viewable_dojos = { dojo.hex_dojo_id:dojo for dojo in Dojos.viewable(user=user) }
emojis = { }
for emoji in Emojis.query.order_by(Emojis.date).all():
if emoji.category not in viewable_dojos:
continue

emojis.setdefault(emoji.user.id, []).append({
"text": emoji.description,
"emoji": emoji.name,
"count": 1,
"url": url_for("pwncollege_dojo.listing", dojo=viewable_dojos[emoji.category].reference_id)
})
emojis = get_viewable_emojis(user)

def standing(item):
if not item:
Expand Down
25 changes: 13 additions & 12 deletions dojo_plugin/pages/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,33 @@
import re

from flask import Blueprint, Response, render_template, abort, url_for
from sqlalchemy.sql import and_
from sqlalchemy.sql import and_, or_
from CTFd.utils.user import get_current_user
from CTFd.utils.decorators import authed_only
from CTFd.models import db, Users, Challenges, Solves
from CTFd.cache import cache

from ..models import Dojos, DojoModules, DojoChallenges
from ..config import DATA_DIR
from ..utils.scores import dojo_scores, module_scores
from ..utils.awards import get_belts, get_viewable_emojis


users = Blueprint("pwncollege_users", __name__)


def view_hacker(user):
current_user_dojos = set(Dojos.viewable(user=get_current_user()))
dojos = [dojo for dojo in Dojos.viewable(user=user) if dojo in current_user_dojos]
if user.hidden:
abort(404)

def ranking(model, user):
solves = db.func.count().label("solves")
rank = db.func.row_number().over(order_by=(solves.desc(), db.func.max(Solves.id))).label("rank")
rankings = model.solves().group_by(Solves.user_id).with_entities(rank, Solves.user_id).all()
user_rank = next((ranking.rank for ranking in rankings if ranking.user_id == user.id), None)
max_rank = len(rankings)
return user_rank, max_rank
dojos = Dojos.query.where(or_(Dojos.official, Dojos.data["type"] == "public")).all()

return render_template("hacker.html", dojos=dojos, user=user, ranking=ranking)
return render_template(
"hacker.html",
dojos=dojos, user=user,
dojo_scores=dojo_scores(), module_scores=module_scores(),
belts=get_belts(), badges=get_viewable_emojis(user)
)

@users.route("/hacker/<int:user_id>")
def view_other(user_id):
Expand Down Expand Up @@ -86,4 +87,4 @@ def view_completion_report(hash):
if not completion_report_path.exists():
abort(404)

return Response(completion_report_path.read_text(), mimetype="text")
return Response(completion_report_path.read_text(), mimetype="text")
15 changes: 15 additions & 0 deletions dojo_plugin/utils/awards.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,18 @@ def update_awards(user):
continue
db.session.add(Emojis(user=user, name=emoji, description=f"Awarded for completing the {dojo_name} dojo.", category=dojo_id))
db.session.commit()

def get_viewable_emojis(user):
viewable_dojos = { dojo.hex_dojo_id:dojo for dojo in Dojos.viewable(user=user) }
emojis = { }
for emoji in Emojis.query.order_by(Emojis.date).all():
if emoji.category not in viewable_dojos:
continue

emojis.setdefault(emoji.user.id, []).append({
"text": emoji.description,
"emoji": emoji.name,
"count": 1,
"url": url_for("pwncollege_dojo.listing", dojo=viewable_dojos[emoji.category].reference_id)
})
return emojis
50 changes: 50 additions & 0 deletions dojo_plugin/utils/scores.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from sqlalchemy.sql import or_
from CTFd.models import Solves, db
from CTFd.cache import cache
from ..models import Dojos, DojoChallenges

@cache.memoize(timeout=1200)
def dojo_scores():
solve_count = db.func.count(Solves.id).label("solve_count")
last_solve_id = db.func.max(Solves.id).label("last_solve_id")
dsc_query = db.session.query(Dojos.id, Solves.user_id, solve_count, last_solve_id).where(
Dojos.dojo_id == DojoChallenges.dojo_id, DojoChallenges.challenge_id == Solves.challenge_id,
or_(Dojos.data["type"] == "public", Dojos.official)
).group_by(Dojos.id, Solves.user_id).order_by(Dojos.id, solve_count.desc(), last_solve_id)

user_ranks = { }
user_solves = { }
dojo_ranks = { }
for dojo_id, user_id, solve_count, last_solve_id in dsc_query:
dojo_ranks.setdefault(dojo_id, [ ]).append(user_id)
user_ranks.setdefault(user_id, {})[dojo_id] = len(dojo_ranks[dojo_id])
user_solves.setdefault(user_id, {})[dojo_id] = solve_count

return {
"user_ranks": user_ranks,
"user_solves": user_solves,
"dojo_ranks": dojo_ranks
}

@cache.memoize(timeout=1200)
def module_scores():
solve_count = db.func.count(Solves.id).label("solve_count")
last_solve_id = db.func.max(Solves.id).label("last_solve_id")
dsc_query = db.session.query(Dojos.id, DojoChallenges.module_index, Solves.user_id, solve_count, last_solve_id).where(
Dojos.dojo_id == DojoChallenges.dojo_id, DojoChallenges.challenge_id == Solves.challenge_id,
or_(Dojos.data["type"] == "public", Dojos.official)
).group_by(Dojos.id, DojoChallenges.module_index, Solves.user_id).order_by(Dojos.id, DojoChallenges.module_index, solve_count.desc(), last_solve_id)

user_ranks = { }
user_solves = { }
module_ranks = { }
for dojo_id, module_idx, user_id, solve_count, last_solve_id in dsc_query:
module_ranks.setdefault(dojo_id, {}).setdefault(module_idx, []).append(user_id)
user_ranks.setdefault(user_id, {}).setdefault(dojo_id, {})[module_idx] = len(module_ranks[dojo_id][module_idx])
user_solves.setdefault(user_id, {}).setdefault(dojo_id, {})[module_idx] = solve_count

return {
"user_ranks": user_ranks,
"user_solves": user_solves,
"module_ranks": module_ranks
}
46 changes: 31 additions & 15 deletions dojo_theme/templates/hacker.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>{{ user.name }}<br><img src="{{ belt }}" class="scoreboard-belt"></h1>
<h1>{{ user.name }}</h1>
<h1>
<a href="{{ url_for("pwncollege_belts.view_belts") }}">
{% if belts.users[user.id] %}
<span title="Earned on {{belts.users[user.id].date}}">
<img src="{{ url_for("views.themes", path="img/dojo/"+belts.users[user.id].color+".svg") }}", class="scoreboard-belt">
</span>
{% else %}
<img src="{{ url_for("views.themes", path="img/dojo/white.svg") }}" class="scoreboard-belt">
{% endif %}
</a>
</h1>
<h2>
{% for badge in badges[user.id] %}
<span title="{{badge.text}}">
<a href="{{badge.url}}">{{badge.emoji}}</a>
</span><span>
</span>
{% endfor %}
</h2>


{% if user.affiliation %}
<h3 class="d-inline-block">
Expand Down Expand Up @@ -39,14 +59,15 @@ <h3 class="d-block">
</div>

<div class="container">
{% for dojo in dojos if dojo.solves(user=user, ignore_visibility=True, ignore_admins=False).count() > 0 %}
{% set rank, max_rank = ranking(dojo, user) %}
{% set solves = dojo.solves(user=user, ignore_visibility=True, ignore_admins=False).all() | map(attribute="challenge_id") | list %}
{% for dojo in dojos if dojo_scores.user_ranks[user.id] and dojo_scores.user_ranks[user.id][dojo.id] %}
{% set rank = dojo_scores.user_ranks[user.id][dojo.id] %}
{% set max_rank = dojo_scores.dojo_ranks[dojo.id] | length %}
{% set solves = dojo_scores.user_solves[user.id][dojo.id] %}
<a class="text-decoration-none" href="{{ url_for('pwncollege_dojo.listing', dojo=dojo.reference_id) }}">
<h2>{{ dojo.name }}</h2>
<h4>
<i class="fas fa-flag pt-3 pr-3" title="Solves"></i>
<td>{{ solves | length }} / {{ dojo.challenges | length }}</td>
<td>{{ solves }} / {{ dojo.challenges | length }}</td>
<i class="fas fa-trophy pt-3 pr-3 pl-5 " title="Rank"></i>
<td>{{ rank or "-" }} / {{ max_rank or "-" }}</td>
</h4>
Expand All @@ -56,25 +77,20 @@ <h4>

<div class="accordion" id="modules-{{dojo.hex_dojo_id}}">
{% for module in dojo.modules %}
{% set solves = module.solves(user=user, ignore_visibility=True, ignore_admins=False).all() | map(attribute="challenge_id") | list %}
{% set rank, max_rank = ranking(module, user) %}
{% set solves = module_scores.user_solves[user.id][dojo.id][module.module_index] %}
{% set rank = module_scores.user_ranks[user.id][dojo.id][module.module_index] %}
{% set max_rank = module_scores.module_ranks[dojo.id][module.module_index] | length %}
{% call(header) accordion_item("modules-{}".format(dojo.hex_dojo_id), loop.index) %}
{% if header %}
<h4 class="accordion-item-name">{{ module.name }}</h4>
<span class="total-solves">
<i class="fas fa-flag pr-1" title="Solves"></i>
<td>{{ solves | length }} / {{ module.challenges | length }}</td>
<td>{{ solves }} / {{ module.challenges | length }}</td>
<i class="fas fa-trophy pr-1 pl-3 " title="Rank"></i>
<td>{{ rank or "-" }} / {{ max_rank or "-" }}</td>
</span>
{% else %}
{% for challenge in module.challenges %}
{% set challenge_status = "challenge-solved" if challenge.challenge_id in solves else "challenge-unsolved" %}
<h5>
<i class="fas fa-flag pr-3 pt-3 {{ challenge_status }}"></i>
<td>{{ challenge.name }}</td>
</h5>
{% endfor %}
TODO
{% endif %}
{% endcall %}
{% endfor %}
Expand Down

0 comments on commit 4893f54

Please sign in to comment.