Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring back the hacker profiles #303

Merged
merged 4 commits into from
Jan 25, 2024
Merged
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
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
Loading