(Translated by https://www.hiragana.jp/)
New charts by lanjelot · Pull Request #40 · hugsy/ctfhub · GitHub
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

New charts #40

Merged
merged 1 commit into from
Mar 18, 2021
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
49 changes: 32 additions & 17 deletions ctfpad/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ def solved_public_challenges(self):
ctf__visibility = "public"
).order_by("solved_time")

@cached_property
def solved_categories(self):
return self.solved_challenges.filter(
ctf__visibility = "public"
).values(
"category__name"
).annotate(
Count("category")
).order_by("category")

@cached_property
def last_solved_challenge(self):
solved = self.solved_public_challenges
Expand All @@ -318,20 +328,32 @@ def best_category(self) -> str:

return best_categories_by_point.first()["category__name"]

@property
@cached_property
def total_scored_percent(self):
if not self.solved_public_challenges:
return 0

member_slices = []
for ctf in filter(lambda c: c.scored_points > 0, Ctf.objects.filter(visibility = "public")):
for ctf in filter(lambda c: c.scored_points > 0, Ctf.objects.filter(visibility="public")):
member_points = 0
for challenge in self.solved_public_challenges.filter(ctf=ctf):
member_points += challenge.points / challenge.solvers.count()
member_slices.append(member_points / ctf.scored_points)

return round(100 * mean(member_slices), 2)

@cached_property
def scored_percent_history(self):
history = []
percent_accu = 0
for ctf in filter(lambda c: c.scored_points > 0, Ctf.objects.filter(visibility="public")):
member_points = 0
for challenge in self.solved_public_challenges.filter(ctf=ctf):
member_points += challenge.points / challenge.solvers.count()
percent_accu += 100 * member_points / ctf.scored_points
history.append((ctf, int(percent_accu)))
return history

@property
def last_logged_in(self):
return self.user.last_login
Expand Down Expand Up @@ -551,38 +573,31 @@ def players_activity(self) -> dict:


def solved_categories(self) -> dict:
"""[summary]

Returns:
dict: [description]
"""Return the total number of challenges solved per category
"""
count_solved_challenges = Challenge.objects.filter(
solvers__isnull=False
).values("category__name").annotate(
dcount=Count("category")
Count("category")
)
return count_solved_challenges


def last_year_stats(self) -> dict:
"""[summary]

Returns:
dict: [description]
"""Return the total number of registered public CTFs per month
"""
res = {}
now = datetime.now()
cur_month = now
cur_month = datetime.now()
for _ in range(12):
start_cur_month = cur_month.replace(day=1)
ctf_by_month = Ctf.objects.filter(
start_date__month = start_cur_month.month
).count()
visibility="public",
challenge__isnull=False,
start_date__month=start_cur_month.month
).distinct().count()
res[start_cur_month.strftime("%Y/%m")] = ctf_by_month
cur_month = start_cur_month - timedelta(days=1)
return res


def get_ranking(self) -> list:
"""Return the all time player ranking
"""
Expand Down
46 changes: 11 additions & 35 deletions ctfpad/templates/ctfpad/stats/charts.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
<script>
function get_random_int(max)
{
return Math.floor(Math.random() * Math.floor(max));
}

function generate_random_rgba()
{
return `rgba(${get_random_int(256)}, ${get_random_int(256)}, ${get_random_int(256)}, 0.2)`;
}

function generate_random_rgb()
{
return `rgb(${get_random_int(256)}, ${get_random_int(256)}, ${get_random_int(256)})`;
}

// https://stackoverflow.com/a/20129594
function generate_random_color(number) {
const hue = number * 137.508; // use golden angle approximation
return `hsl(${hue},50%,75%)`;
}
</script>


<div class="row" id="stats">
<div class="col-md-4">
<div class="card card-body">
Expand All @@ -33,17 +9,17 @@
"data":
{
"labels": [
{% for player in player_ctf_count %}
"{{player}}",
{% endfor %}
{% for player in player_ctf_count %}
"{{player}}",
{% endfor %}
],
"datasets": [
{
"label": "Most active players*",
"data": [
{% for player, count in player_ctf_count.items %}
{{count}},
{% endfor %}
{% for player, count in player_ctf_count.items %}
{{count}},
{% endfor %}
],
"fill": false,
"borderWidth": 1,
Expand Down Expand Up @@ -79,16 +55,16 @@
"data": {
"labels": [
{% for solved in most_solved %}
"{{solved.category__name|upper}}",
"{{solved.category__name|upper}}",
{% endfor %}
],
"datasets": [{
"label": "Most solved categories",
"data": [
{% for solved in most_solved %}
{{solved.dcount|upper}},
{{solved.category__count|upper}},
{% endfor %}
],
],
"backgroundColor": [{% for _ in most_solved %}generate_random_color({{forloop.counter}}), {% endfor %}]
}]
}
Expand All @@ -106,12 +82,12 @@
"type": "line",
"data": {
"labels": [
{% for month in last_year_stats reversed %}"{{month}}",{%endfor%}
{% for month in last_year_stats reversed %}"{{month}}",{% endfor %}
],
"datasets": [{
"label": "Number of CTFs played for the last year",
"data": [
{% for month, count in last_year_stats.items reversed %}{{count}},{%endfor%}
{% for month, count in last_year_stats.items reversed %}{{count}},{% endfor %}
],
"fill": false,
"borderColor": "rgb(75, 192, 192)",
Expand Down
10 changes: 8 additions & 2 deletions ctfpad/templates/ctfpad/stats/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ <h4 class="card-title">Team Information</h4>
<div>
<ul class="nav nav-tabs">
<li class="nav-item">
<a href="#team" class="nav-link active" data-toggle="tab">Global</a>
<a href="#team" class="nav-link active" data-toggle="tab">General</a>
</li>
<li class="nav-item">
<a href="#charts" class="nav-link" data-toggle="tab">Stats</a>
</li>
<li class="nav-item">
<a href="#rank" class="nav-link" data-toggle="tab">Player Ranking</a>
<a href="#rank" class="nav-link" data-toggle="tab">Ranking</a>
</li>
<li class="nav-item">
<a href="#timeline" class="nav-link" data-toggle="tab">Timeline</a>
</li>
</ul>
</div>
Expand All @@ -47,6 +50,9 @@ <h4 class="card-title">Team Information</h4>
<div class="tab-pane fade" id="rank">
{% include 'ctfpad/stats/rank.html' %}
</div>
<div class="tab-pane fade" id="timeline">
{% include 'ctfpad/stats/timeline.html' %}
</div>
</div>
</div>
<div class="col-sm-1"></div>
Expand Down
4 changes: 2 additions & 2 deletions ctfpad/templates/ctfpad/stats/rank.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h5>All Time Ranking</h5>
{% for member in ranked_members %}
<tr>
<th scope="row">{{ forloop.counter | ordinal }}</th>
<td>{{ member.username }}</td>
<td><a href="{% url 'ctfpad:users-detail' member.id %}">{{ member.username }}</a></td>
<td>{{ member.total_scored_percent }}</td>
<td>{{ member.best_category | lower }}</td>
</tr>
Expand Down Expand Up @@ -78,7 +78,7 @@ <h5>Last CTFs</h5>
{% for member in ranked %}
<td style="vertical-align: middle;">
{% if member %}
<img width="30px" height="30px" src="{{member.avatar_url}}" title="{{member.username}}" alt="avatar of " class="rounded-circle">
<a href="{% url 'ctfpad:users-detail' member.id %}"><img width="30px" height="30px" src="{{member.avatar_url}}" title="{{member.username}}" alt="avatar of " class="rounded-circle"></a>
&nbsp;{{member.scored_percent}}&#x25;
{% else %}
&nbsp;
Expand Down
34 changes: 34 additions & 0 deletions ctfpad/templates/ctfpad/stats/timeline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

<div class="row" style="width: 100%;">
<div class="card card-body">
<canvas id="timeline_chart"></canvas>
<script>
new Chart(document.getElementById("timeline_chart"), {
"type": "line",
"data": {
"labels": [
{% for ctf, _ in ranked_members.0.scored_percent_history %}
"{{ ctf.name }}",
{% endfor %}
],
"datasets": [
{% for member in ranked_members %}
{
"label": "{{ member.username }}",
"data": [
{% for _, accu in member.scored_percent_history %}{{ accu }}, {% endfor %}
],
"fill": false,
"lineTension": 0,
"borderColor": generate_random_color({{ forloop.counter }}),
},
{% endfor %}
]
},
"options": {

}
});
</script>
</div>
</div>
40 changes: 35 additions & 5 deletions ctfpad/templates/users/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,28 +147,58 @@ <h5>Accomplishments</h5>

<ul class="nav nav-tabs">
<li class="nav-item">
<a href="#stats" class="nav-link active" data-toggle="tab">Overview</a>
<a href="#categories" class="nav-link active" data-toggle="tab">Categories</a>
</li>
<li class="nav-item">
<a href="#detail" class="nav-link" data-toggle="tab">Detail</a>
<a href="#stats" class="nav-link" data-toggle="tab">Timeline</a>
</li>
<li class="nav-item">
<a href="#detail" class="nav-link" data-toggle="tab">History</a>
</li>
</ul>

<div class="tab-content" style="margin-top: 17px;">
<div class="tab-pane fade show active" id="stats">

<div class="tab-pane fade show active" id="categories">
<div id="wrapper-chart-user-stats">
<canvas id="user-categories"></canvas>
<script>
new Chart(document.getElementById("user-categories"), {
"type": "doughnut",
"data": {
"labels": [
{% for c in member.solved_categories %}
"{{c.category__name|upper}}",
{% endfor %}
],
"datasets": [{
"label": "Solved categories",
"data": [
{% for c in member.solved_categories %}
{{c.category__count|upper}},
{% endfor %}
],
"backgroundColor": [ {% for _ in member.solved_categories %}generate_random_color({{forloop.counter}}), {% endfor %}]
}]
}
});
</script>
</div>
</div>
<div class="tab-pane fade" id="stats">
<div id="wrapper-chart-user-stats">
<canvas id="user-stats"></canvas>
<script>
new Chart(document.getElementById("user-stats"), {
"type": "line",
"data": {
"labels": [
{% for solved in member.solved_public_challenges.all|as_time_accumulator_graph %}"{{solved.time}}",{%endfor%}
{% for solved in member.solved_public_challenges.all|as_time_accumulator_graph %}"{{solved.time}}",{% endfor %}
],
"datasets": [{
"label": "Scored points",
"data": [
{% for solved in member.solved_challenges.all|as_time_accumulator_graph %}{{solved.accu}}, {%endfor%}
{% for solved in member.solved_public_challenges.all|as_time_accumulator_graph %}{{solved.accu}}, {% endfor %}
],
"fill": false,
"borderColor": "rgb(75, 192, 192)",
Expand Down
23 changes: 21 additions & 2 deletions static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ function generate_random_string(length = 12)
return str;
}


function toggle_input_password_visibility()
{
for(let r of document.getElementsByClassName("reveal") )
Expand All @@ -19,4 +18,24 @@ function toggle_input_password_visibility()
else if (r.type === "text")
r.type = "password";
}
}
}

function generate_random_color(number) {
const hue = number * 137.508; // use golden angle approximation
return `hsl(${hue},50%,75%)`;
}

// function get_random_int(max)
// {
// return Math.floor(Math.random() * Math.floor(max));
// }

// function generate_random_rgba()
// {
// return `rgba(${get_random_int(256)}, ${get_random_int(256)}, ${get_random_int(256)}, 0.2)`;
// }

// function generate_random_rgb()
// {
// return `rgb(${get_random_int(256)}, ${get_random_int(256)}, ${get_random_int(256)})`;
// }