diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index d2509a06..d38149a8 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -23,7 +23,6 @@ from flask import request, jsonify, current_app, Response from flask_login import current_user, login_required from sqlalchemy.orm import joinedload from sqlalchemy.sql.expression import func -from werkzeug.datastructures import ResponseCacheControl from app import csrf from app.logic.graphs import get_package_stats, get_package_stats_for_user, get_all_package_stats @@ -31,7 +30,7 @@ from app.markdown import render_markdown from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, \ MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread 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, get_request_date from . import bp from .auth import is_api_authd from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \ @@ -483,7 +482,9 @@ def list_all_reviews(): @cors_allowed @cached(300) def package_stats(package: Package): - return jsonify(get_package_stats(package)) + start = get_request_date("start") + end = get_request_date("end") + return jsonify(get_package_stats(package, start, end)) @bp.route("/api/package_stats/") @@ -645,7 +646,9 @@ def user_stats(username: str): if user is None: error(404, "User not found") - return jsonify(get_package_stats_for_user(user)) + start = get_request_date("start") + end = get_request_date("end") + return jsonify(get_package_stats_for_user(user, start, end)) @bp.route("/api/cdb_schema/") diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index a7a9091a..467d6931 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -725,8 +725,12 @@ def game_support(package): @bp.route("/packages///stats/") @is_package_page def statistics(package): + start = request.args.get("start") + end = request.args.get("end") + return render_template("packages/stats.html", - package=package, tabs=get_package_tabs(current_user, package), current_tab="stats") + package=package, tabs=get_package_tabs(current_user, package), current_tab="stats", + start=start, end=end, options=get_daterange_options(), noindex=start or end) @bp.route("/packages///stats.csv") diff --git a/app/blueprints/users/profile.py b/app/blueprints/users/profile.py index 2ba42cd9..a9d4fceb 100644 --- a/app/blueprints/users/profile.py +++ b/app/blueprints/users/profile.py @@ -23,8 +23,8 @@ from flask_login import current_user, login_required from sqlalchemy import func from app.models import * -from app.tasks.forumtasks import checkForumAccount from . import bp +from ...utils import get_daterange_options @bp.route("/users/", methods=["GET"]) @@ -249,4 +249,7 @@ def statistics(username): downloads = db.session.query(func.sum(Package.downloads)).filter(Package.author==user).one() - return render_template("users/stats.html", user=user, downloads=downloads[0]) + start = request.args.get("start") + end = request.args.get("end") + return render_template("users/stats.html", user=user, downloads=downloads[0], + start=start, end=end, options=get_daterange_options(), noindex=start or end) diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index 6cfd9697..6c1243d7 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -125,8 +125,11 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). * 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. + * Query args: + * `start`: start date, inclusive. Optional. Default: 2022-10-01. UTC. + * `end`: end date, inclusive. Optional. Default: today. UTC. * An object with the following keys: - * `start`: start date, inclusive. Ex: 2022-10-22. + * `start`: start date, inclusive. Ex: 2022-10-22. M * `end`: end date, inclusive. Ex: 2022-11-05. * `platform_minetest`: list of integers per day. * `platform_other`: list of integers per day. @@ -374,6 +377,9 @@ Example: * Returns daily stats for the user's packages, or null if there is no data. * Daily date is done based on the UTC timezone. * EXPERIMENTAL. This API may change without warning. + * Query args: + * `start`: start date, inclusive. Optional. Default: 2022-10-01. UTC. + * `end`: end date, inclusive. Optional. Default: today. UTC. * A table with the following keys: * `from`: start date, inclusive. Ex: 2022-10-22. * `end`: end date, inclusive. Ex: 2022-11-05. diff --git a/app/logic/graphs.py b/app/logic/graphs.py index 8b9204a8..905e630c 100644 --- a/app/logic/graphs.py +++ b/app/logic/graphs.py @@ -41,24 +41,36 @@ def _flatten_data(stats): return result -def get_package_stats(package: Package): - stats = package.daily_stats.order_by(db.asc(PackageDailyStats.date)).all() +def get_package_stats(package: Package, start_date: Optional[datetime.date], end_date: Optional[datetime.date]): + query = package.daily_stats.order_by(db.asc(PackageDailyStats.date)) + if start_date: + query = query.filter(PackageDailyStats.date >= start_date) + if end_date: + query = query.filter(PackageDailyStats.date <= end_date) + + stats = query.all() if len(stats) == 0: return None return _flatten_data(stats) -def get_package_stats_for_user(user: User): - stats = db.session \ +def get_package_stats_for_user(user: User, start_date: Optional[datetime.date], end_date: Optional[datetime.date]): + query = 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)) \ + .filter(PackageDailyStats.package.has(author_id=user.id)) + + if start_date: + query = query.filter(PackageDailyStats.date >= start_date) + if end_date: + query = query.filter(PackageDailyStats.date <= end_date) + + stats = query.order_by(db.asc(PackageDailyStats.date)) \ .group_by(PackageDailyStats.date) \ .all() if len(stats) == 0: @@ -122,9 +134,15 @@ def get_package_overview_for_user(user: Optional[User], start_date: datetime.dat return result -def get_all_package_stats(): - end_date = datetime.datetime.utcnow().date() - start_date = (datetime.datetime.utcnow() - datetime.timedelta(days=29)).date() +def get_all_package_stats(start_date: Optional[datetime.date], end_date: Optional[datetime.date]): + now_date = datetime.datetime.utcnow().date() + if end_date is None or end_date > now_date: + end_date = now_date + + min_start_date = (datetime.datetime.utcnow() - datetime.timedelta(days=29)).date() + if start_date is None or start_date < min_start_date: + start_date = min_start_date + return { "start": start_date.isoformat(), "end": end_date.isoformat(), diff --git a/app/templates/macros/stats.html b/app/templates/macros/stats.html index e2c8b96f..79ebd2ee 100644 --- a/app/templates/macros/stats.html +++ b/app/templates/macros/stats.html @@ -30,6 +30,20 @@ {% endmacro %} +{% macro render_daterange_selector(options) %} + +{% endmacro %} + + {% macro render_package_stats(source, downloads) %}