Statistics: Fix issue with missing days

Fixes #397
This commit is contained in:
rubenwardy 2022-11-09 18:46:11 +00:00
parent 852e6ab5a0
commit e15a3c682f
5 changed files with 115 additions and 17 deletions

@ -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 }));
} }
{ {

@ -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]