mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-10 15:07:35 +01:00
parent
852e6ab5a0
commit
e15a3c682f
@ -29,14 +29,13 @@ from app.models import Tag, PackageState, PackageType, Package, db, PackageRelea
|
|||||||
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
|
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread
|
||||||
from app.querybuilder import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
|
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
|
||||||
|
from app.logic.graphs import get_package_stats, get_package_stats_for_user
|
||||||
from . import bp
|
from . import bp
|
||||||
from .auth import is_api_authd
|
from .auth import is_api_authd
|
||||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
|
||||||
api_order_screenshots, api_edit_package, api_set_cover_image
|
api_order_screenshots, api_edit_package, api_set_cover_image
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from ...logic.graphs import flatten_data
|
|
||||||
|
|
||||||
|
|
||||||
def cors_allowed(f):
|
def cors_allowed(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
@ -439,7 +438,7 @@ def list_all_reviews():
|
|||||||
@is_package_page
|
@is_package_page
|
||||||
@cors_allowed
|
@cors_allowed
|
||||||
def package_stats(package: Package):
|
def package_stats(package: Package):
|
||||||
return jsonify(flatten_data(package))
|
return jsonify(get_package_stats(package))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/scores/")
|
@bp.route("/api/scores/")
|
||||||
@ -577,3 +576,13 @@ def all_deps():
|
|||||||
},
|
},
|
||||||
"items": [format_pkg(pkg) for pkg in pagination.items],
|
"items": [format_pkg(pkg) for pkg in pagination.items],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/users/<username>/stats/")
|
||||||
|
@cors_allowed
|
||||||
|
def user_stats(username: str):
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user is None:
|
||||||
|
error(404, "User not found")
|
||||||
|
|
||||||
|
return jsonify(get_package_stats_for_user(user))
|
||||||
|
@ -110,14 +110,16 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
|||||||
* Same as above.
|
* Same as above.
|
||||||
* GET `/api/packages/<username>/<name>/stats/`
|
* GET `/api/packages/<username>/<name>/stats/`
|
||||||
* Returns daily stats for package, or null if there is no data.
|
* Returns daily stats for package, or null if there is no data.
|
||||||
|
* Daily date is done based on the UTC timezone.
|
||||||
* EXPERIMENTAL. This API may change without warning.
|
* EXPERIMENTAL. This API may change without warning.
|
||||||
* A table with the following keys:
|
* A table with the following keys:
|
||||||
* `dates`: list of dates in isoformat
|
* `from`: start date, inclusive. Ex: 2022-10-22.
|
||||||
* `platform_minetest`: list of integers per date
|
* `end`: end date, inclusive. Ex: 2022-11-05.
|
||||||
* `platform_other`: list of integers per date
|
* `platform_minetest`: list of integers per day.
|
||||||
* `reason_new`: list of integers per date
|
* `platform_other`: list of integers per day.
|
||||||
* `reason_dependency`: list of integers per date
|
* `reason_new`: list of integers per day.
|
||||||
* `reason_update`: list of integers per date
|
* `reason_dependency`: list of integers per day.
|
||||||
|
* `reason_update`: list of integers per day.
|
||||||
|
|
||||||
You can download a package by building one of the two URLs:
|
You can download a package by building one of the two URLs:
|
||||||
|
|
||||||
|
@ -1,17 +1,62 @@
|
|||||||
from app.models import Package, PackageDailyStats, db
|
from datetime import timedelta
|
||||||
|
from app.models import User, Package, PackageDailyStats, db
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
def flatten_data(package: Package):
|
def daterange(start_date, end_date):
|
||||||
stats = package.daily_stats.order_by(db.asc(PackageDailyStats.date)).all()
|
for n in range(int((end_date - start_date).days) + 1):
|
||||||
|
yield start_date + timedelta(n)
|
||||||
|
|
||||||
|
|
||||||
|
keys = ["platform_minetest", "platform_other", "reason_new",
|
||||||
|
"reason_dependency", "reason_update"]
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_data(stats):
|
||||||
if len(stats) == 0:
|
if len(stats) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
start_date = stats[0].date
|
||||||
|
end_date = stats[-1].date
|
||||||
result = {
|
result = {
|
||||||
"dates": [stat.date.isoformat() for stat in stats],
|
"start": start_date.isoformat(),
|
||||||
|
"end": end_date.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in ["platform_minetest", "platform_other", "reason_new",
|
for key in keys:
|
||||||
"reason_dependency", "reason_update"]:
|
result[key] = []
|
||||||
result[key] = [getattr(stat, key) for stat in stats]
|
|
||||||
|
i = 0
|
||||||
|
for date in daterange(start_date, end_date):
|
||||||
|
stat = stats[i]
|
||||||
|
if stat.date == date:
|
||||||
|
for key in keys:
|
||||||
|
result[key].append(getattr(stat, key))
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
for key in keys:
|
||||||
|
result[key].append(0)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_stats(package: Package):
|
||||||
|
stats = package.daily_stats.order_by(db.asc(PackageDailyStats.date)).all()
|
||||||
|
return _flatten_data(stats)
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_stats_for_user(user: User):
|
||||||
|
stats = db.session \
|
||||||
|
.query(PackageDailyStats.date,
|
||||||
|
func.sum(PackageDailyStats.platform_minetest).label("platform_minetest"),
|
||||||
|
func.sum(PackageDailyStats.platform_other).label("platform_other"),
|
||||||
|
func.sum(PackageDailyStats.reason_new).label("reason_new"),
|
||||||
|
func.sum(PackageDailyStats.reason_dependency).label("reason_dependency"),
|
||||||
|
func.sum(PackageDailyStats.reason_update).label("reason_update")) \
|
||||||
|
.filter(PackageDailyStats.package.has(author_id=user.id)) \
|
||||||
|
.order_by(db.asc(PackageDailyStats.date)) \
|
||||||
|
.group_by(PackageDailyStats.date) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
return _flatten_data(stats)
|
||||||
|
@ -32,6 +32,7 @@ function sum(list) {
|
|||||||
|
|
||||||
const chartColorsBg = chartColors.map(color => `rgba(${hexToRgb(color.slice(1))}, 0.2)`);
|
const chartColorsBg = chartColors.map(color => `rgba(${hexToRgb(color.slice(1))}, 0.2)`);
|
||||||
|
|
||||||
|
const SECONDS_IN_A_DAY = 1000 * 3600 * 24;
|
||||||
|
|
||||||
async function load_data() {
|
async function load_data() {
|
||||||
const root = document.getElementById("stats-root");
|
const root = document.getElementById("stats-root");
|
||||||
@ -46,6 +47,14 @@ async function load_data() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(json.start);
|
||||||
|
const endDate = new Date(json.end);
|
||||||
|
const numberOfDays = Math.round((endDate.valueOf() - startDate.valueOf()) / SECONDS_IN_A_DAY) + 1;
|
||||||
|
const dates = [...Array(numberOfDays)].map((_, i) => {
|
||||||
|
const date = new Date(startDate.valueOf() + i*SECONDS_IN_A_DAY);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
});
|
||||||
|
|
||||||
const total7 = sum(json.platform_minetest.slice(-7)) + sum(json.platform_other.slice(-7));
|
const total7 = sum(json.platform_minetest.slice(-7)) + sum(json.platform_other.slice(-7));
|
||||||
document.getElementById("downloads_total7d").textContent = total7;
|
document.getElementById("downloads_total7d").textContent = total7;
|
||||||
document.getElementById("downloads_avg7d").textContent = (total7 / 7).toFixed(0);
|
document.getElementById("downloads_avg7d").textContent = (total7 / 7).toFixed(0);
|
||||||
@ -66,7 +75,7 @@ async function load_data() {
|
|||||||
root.style.display = "block";
|
root.style.display = "block";
|
||||||
|
|
||||||
function getData(list) {
|
function getData(list) {
|
||||||
return list.map((value, i) => ({ x: json.dates[i], y: value }));
|
return list.map((value, i) => ({ x: dates[i], y: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
33
app/tests/unit/test_logic_graphs.py
Normal file
33
app/tests/unit/test_logic_graphs.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from app.logic.graphs import _flatten_data
|
||||||
|
|
||||||
|
|
||||||
|
class DailyStat:
|
||||||
|
date: datetime.date
|
||||||
|
platform_minetest: int
|
||||||
|
platform_other: int
|
||||||
|
reason_new: int
|
||||||
|
reason_dependency: int
|
||||||
|
reason_update: int
|
||||||
|
|
||||||
|
def __init__(self, date: str, x: int):
|
||||||
|
self.date = datetime.date.fromisoformat(date)
|
||||||
|
self.platform_minetest = x
|
||||||
|
self.platform_other = 0
|
||||||
|
self.reason_new = 0
|
||||||
|
self.reason_dependency = 0
|
||||||
|
self.reason_update = 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_flatten_data():
|
||||||
|
res = _flatten_data([
|
||||||
|
DailyStat("2022-03-28", 3),
|
||||||
|
DailyStat("2022-03-29", 10),
|
||||||
|
DailyStat("2022-04-01", 5),
|
||||||
|
DailyStat("2022-04-02", 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
assert res["start"] == datetime.date.fromisoformat("2022-03-28")
|
||||||
|
assert res["end"] == datetime.date.fromisoformat("2022-04-02")
|
||||||
|
assert res["platform_minetest"] == [3, 10, 0, 0, 5, 1]
|
Loading…
Reference in New Issue
Block a user