mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-24 06:51:29 +01:00
Add statistics page
This commit is contained in:
parent
5c0480b39d
commit
4387e71417
@ -52,6 +52,11 @@ def get_package_tabs(user: User, package: Package):
|
|||||||
"title": gettext("Audit Log"),
|
"title": gettext("Audit Log"),
|
||||||
"url": package.getURL("packages.audit")
|
"url": package.getURL("packages.audit")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "stats",
|
||||||
|
"title": gettext("Statistics"),
|
||||||
|
"url": package.getURL("packages.stats")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "share",
|
"id": "share",
|
||||||
"title": gettext("Share and Badges"),
|
"title": gettext("Share and Badges"),
|
||||||
|
@ -708,3 +708,10 @@ def game_support(package):
|
|||||||
return render_template("packages/game_support.html", package=package, form=form,
|
return render_template("packages/game_support.html", package=package, form=form,
|
||||||
mod_conf_lines=mod_conf_lines, force_game_detection=force_game_detection,
|
mod_conf_lines=mod_conf_lines, force_game_detection=force_game_detection,
|
||||||
tabs=get_package_tabs(current_user, package), current_tab="game_support")
|
tabs=get_package_tabs(current_user, package), current_tab="game_support")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/packages/<author>/<name>/stats/")
|
||||||
|
@is_package_page
|
||||||
|
def stats(package):
|
||||||
|
return render_template("packages/stats.html",
|
||||||
|
package=package, tabs=get_package_tabs(current_user, package), current_tab="stats")
|
||||||
|
13
app/public/static/libs/chart.min.js
vendored
Normal file
13
app/public/static/libs/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
app/public/static/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
7
app/public/static/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
185
app/public/static/package_stats.js
Normal file
185
app/public/static/package_stats.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
const labelColor = "#bbb";
|
||||||
|
const gridColor = "#333";
|
||||||
|
|
||||||
|
|
||||||
|
const chartColors = [
|
||||||
|
"#7eb26d",
|
||||||
|
"#eab839",
|
||||||
|
"#6ed0e0",
|
||||||
|
"#e24d42",
|
||||||
|
"#1f78c1",
|
||||||
|
"#ba43a9",
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
var bigint = parseInt(hex, 16);
|
||||||
|
var r = (bigint >> 16) & 255;
|
||||||
|
var g = (bigint >> 8) & 255;
|
||||||
|
var b = bigint & 255;
|
||||||
|
|
||||||
|
return r + "," + g + "," + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const chartColorsBg = chartColors.map(color => `rgba(${hexToRgb(color.slice(1))}, 0.2)`);
|
||||||
|
|
||||||
|
|
||||||
|
async function load_data() {
|
||||||
|
const root = document.getElementById("stats-root");
|
||||||
|
const source = root.getAttribute("data-source");
|
||||||
|
const response = await fetch(source);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const jsonOther = json.platform_minetest.map((value, i) =>
|
||||||
|
value + json.platform_other[i]
|
||||||
|
- json.reason_new[i] - json.reason_dependency[i]
|
||||||
|
- json.reason_update[i]);
|
||||||
|
|
||||||
|
document.getElementById("loading").style.display = "none";
|
||||||
|
|
||||||
|
function getData(list) {
|
||||||
|
return list.map((value, i) => ({ x: json.dates[i], y: value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sum(list) {
|
||||||
|
return list.reduce((acc, x) => acc + x, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ctx = document.getElementById("chart-platform").getContext("2d");
|
||||||
|
const data = {
|
||||||
|
datasets: [
|
||||||
|
{ label: "Web / other", data: getData(json.platform_other) },
|
||||||
|
{ label: "Minetest", data: getData(json.platform_minetest) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setup_chart(ctx, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ctx = document.getElementById("chart-reason").getContext("2d");
|
||||||
|
const data = {
|
||||||
|
datasets: [
|
||||||
|
{ label: "Other / Unknown", data: getData(jsonOther) },
|
||||||
|
{ label: "Update", data: getData(json.reason_update) },
|
||||||
|
{ label: "Dependency", data: getData(json.reason_dependency) },
|
||||||
|
{ label: "New Install", data: getData(json.reason_new) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setup_chart(ctx, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ctx = document.getElementById("chart-reason-pie").getContext("2d");
|
||||||
|
const data = {
|
||||||
|
labels: [
|
||||||
|
"New Install",
|
||||||
|
"Dependency",
|
||||||
|
"Update",
|
||||||
|
"Other / Unknown",
|
||||||
|
],
|
||||||
|
datasets: [{
|
||||||
|
label: "My First Dataset",
|
||||||
|
data: [
|
||||||
|
sum(json.reason_new),
|
||||||
|
sum(json.reason_dependency),
|
||||||
|
sum(json.reason_update),
|
||||||
|
sum(jsonOther),
|
||||||
|
],
|
||||||
|
backgroundColor: chartColors,
|
||||||
|
hoverOffset: 4,
|
||||||
|
borderWidth: 0,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
type: "doughnut",
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "right",
|
||||||
|
labels: {
|
||||||
|
color: labelColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Chart(ctx, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setup_chart(ctx, data) {
|
||||||
|
data.datasets = data.datasets.map((set, i) => {
|
||||||
|
const colorIdx = data.datasets.length - i - 1;
|
||||||
|
return {
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartColorsBg[colorIdx],
|
||||||
|
borderColor: chartColors[colorIdx],
|
||||||
|
pointBackgroundColor: chartColors[colorIdx],
|
||||||
|
...set,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: "line",
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "index"
|
||||||
|
},
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
reverse: true,
|
||||||
|
labels: {
|
||||||
|
color: labelColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: "nearest",
|
||||||
|
axis: "x",
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: "time",
|
||||||
|
time: {
|
||||||
|
// min: start,
|
||||||
|
// max: end,
|
||||||
|
unit: "day",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: labelColor,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
min: 0,
|
||||||
|
precision: 0,
|
||||||
|
ticks: {
|
||||||
|
color: labelColor,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new Chart(ctx, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(load_data);
|
@ -272,3 +272,8 @@ blockquote {
|
|||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=35">
|
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=36">
|
||||||
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||||
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
|
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
|
||||||
<link rel="icon" href="/favicon-128.png" sizes="128x128">
|
<link rel="icon" href="/favicon-128.png" sizes="128x128">
|
||||||
|
54
app/templates/packages/stats.html
Normal file
54
app/templates/packages/stats.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "packages/package_base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ _("Statistics") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
<script src="/static/libs/chart.min.js"></script>
|
||||||
|
<script src="/static/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||||
|
<script src="/static/package_stats.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mt-0">{{ self.title() }}</h2>
|
||||||
|
<noscript>
|
||||||
|
<p class="alert alert-danger">
|
||||||
|
{{ _("JavaScript is needed for graphs") }}
|
||||||
|
</p>
|
||||||
|
</noscript>
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body media align-items-center">
|
||||||
|
<i class="fas fa-download ml-2 mr-4 text-size" style="font-size: 45px; color: #999;"></i>
|
||||||
|
<div class="media-body">
|
||||||
|
<div class="mt-0 h4">
|
||||||
|
{{ package.downloads }}
|
||||||
|
</div>
|
||||||
|
<div class="my-0">
|
||||||
|
{{ _("Lifetime downloads") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loading">{{ _("Loading...") }}</div>
|
||||||
|
<div id="stats-root" data-source="{{ package.getURL('api.package_stats') }}">
|
||||||
|
<h3>{{ _("Downloads") }}</h3>
|
||||||
|
|
||||||
|
<h4>{{ _("Client") }}</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ _("This is a stacked area graph. For total downloads, look at the combined height.") }}
|
||||||
|
</p>
|
||||||
|
<canvas id="chart-platform" class="chart"></canvas>
|
||||||
|
|
||||||
|
<h4 class="mt-5">{{ _("Reason") }}</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ _("This is a stacked area graph. For total downloads, look at the combined height.") }}
|
||||||
|
</p>
|
||||||
|
<canvas id="chart-reason" class="chart"></canvas>
|
||||||
|
<canvas id="chart-reason-pie" class="chart mt-4"></canvas>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -164,8 +164,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% if release %}
|
{% if release %}
|
||||||
<a class="btn" rel="nofollow" href="{{ package.getURL("packages.download") }}" title="{{ _("Downloads") }}"
|
<a class="btn" rel="nofollow" href="{{ package.getURL('packages.stats') }}" title="{{ _('Statistics') }}">
|
||||||
download="{{ release.getDownloadFileName() }}">
|
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
<span class="count">{{ package.downloads }}</span>
|
<span class="count">{{ package.downloads }}</span>
|
||||||
</a>
|
</a>
|
||||||
@ -544,16 +543,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
|
<a href="{{ package.getURL('packages.stats') }}">
|
||||||
|
<i class="fas fa-chart-line mr-1"></i>
|
||||||
|
{{ _("Statistics") }}
|
||||||
|
</a>
|
||||||
{% if package.approved and current_user != package.author %}
|
{% if package.approved and current_user != package.author %}
|
||||||
|
|
|
||||||
<a href="{{ url_for('report.report', url=url_current()) }}">
|
<a href="{{ url_for('report.report', url=url_current()) }}">
|
||||||
<i class="fas fa-flag mr-1"></i>
|
<i class="fas fa-flag mr-1"></i>
|
||||||
{{ _("Report") }}
|
{{ _("Report") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if package.checkPerm(current_user, "EDIT_PACKAGE") or package.checkPerm(current_user, "APPROVE_NEW") %}
|
{% if package.checkPerm(current_user, "EDIT_PACKAGE") or package.checkPerm(current_user, "APPROVE_NEW") %}
|
||||||
{% if package.approved and current_user != package.author %}
|
|
||||||
|
|
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ package.getURL('packages.audit') }}">
|
<a href="{{ package.getURL('packages.audit') }}">
|
||||||
{{ _("See audit log") }}
|
{{ _("See audit log") }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -82,7 +82,6 @@ def url_set_query(**kwargs):
|
|||||||
else:
|
else:
|
||||||
args.setlist(key, [ value ])
|
args.setlist(key, [ value ])
|
||||||
|
|
||||||
|
|
||||||
dargs = dict(args.lists())
|
dargs = dict(args.lists())
|
||||||
if request.view_args:
|
if request.view_args:
|
||||||
dargs.update(request.view_args)
|
dargs.update(request.view_args)
|
||||||
|
Loading…
Reference in New Issue
Block a user