From d11d6381449b7a2fbe82d46367f55c122ad268a1 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Sat, 5 Nov 2022 22:39:33 +0000 Subject: [PATCH] Add per-package download tracking --- app/blueprints/packages/releases.py | 5 ++- app/models/packages.py | 47 ++++++++++++++++++++++++++++ app/tests/unit/test_utils.py | 5 +++ migrations/versions/ea83ce985e55_.py | 34 ++++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/tests/unit/test_utils.py create mode 100644 migrations/versions/ea83ce985e55_.py diff --git a/app/blueprints/packages/releases.py b/app/blueprints/packages/releases.py index 6079f5de..acebfbf1 100644 --- a/app/blueprints/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -114,6 +114,9 @@ def download_release(package, id): ip = request.headers.get("X-Forwarded-For") or request.remote_addr if ip is not None and not is_user_bot(): + is_minetest = (request.headers.get("User-Agent") or "").startswith("Minetest") + PackageDailyStats.update(package, is_minetest, request.args.get("reason")) + key = make_download_key(ip, release.package) if not has_key(key): set_key(key, "true") @@ -130,7 +133,7 @@ def download_release(package, id): "score": Package.score + bonus }) - db.session.commit() + db.session.commit() return redirect(release.url) diff --git a/app/models/packages.py b/app/models/packages.py index a1391897..eee5b38c 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -23,6 +23,7 @@ from flask_babel import lazy_gettext from flask_sqlalchemy import BaseQuery from sqlalchemy_searchable import SearchQueryMixin from sqlalchemy_utils.types import TSVectorType +from sqlalchemy.dialects.postgresql import insert from . import db from .users import Permission, UserRank, User @@ -1200,3 +1201,49 @@ class PackageAlias(db.Model): def getAsDictionary(self): return f"{self.author}/{self.name}" + + +class PackageDailyStats(db.Model): + package_id = db.Column(db.Integer, db.ForeignKey("package.id"), primary_key=True) + package = db.relationship("Package", foreign_keys=[package_id]) + date = db.Column(db.Date, primary_key=True) + + platform_minetest = db.Column(db.Integer, nullable=False, default=0) + platform_other = db.Column(db.Integer, nullable=False, default=0) + + reason_new = db.Column(db.Integer, nullable=False, default=0) + reason_dependency = db.Column(db.Integer, nullable=False, default=0) + reason_update = db.Column(db.Integer, nullable=False, default=0) + + @staticmethod + def update(package: Package, is_minetest: bool, reason: str): + date = datetime.date.today() + to_update = dict() + kwargs = { + "package_id": package.id, "date": date + } + + field_platform = "platform_minetest" if is_minetest else "platform_other" + to_update[field_platform] = getattr(PackageDailyStats, field_platform) + 1 + kwargs[field_platform] = 1 + + field_reason = None + if reason == "new": + field_reason = "reason_new" + elif reason == "dep": + field_reason = "reason_dependency" + elif reason == "update": + field_reason = "./reason_update" + + if field_reason: + to_update[field_reason] = getattr(PackageDailyStats, field_reason) + 1 + kwargs[field_reason] = 1 + + stmt = insert(PackageDailyStats).values(**kwargs) + stmt = stmt.on_conflict_do_update( + index_elements=[PackageDailyStats.package_id, PackageDailyStats.date], + set_=to_update + ) + + conn = db.session.connection() + conn.execute(stmt) diff --git a/app/tests/unit/test_utils.py b/app/tests/unit/test_utils.py new file mode 100644 index 00000000..88fec7d3 --- /dev/null +++ b/app/tests/unit/test_utils.py @@ -0,0 +1,5 @@ +import user_agents + + +def test_minetest_is_not_bot(): + assert not user_agents.parse("Minetest/5.5.1 (Linux/4.14.193+-ab49821 aarch64)").is_bot diff --git a/migrations/versions/ea83ce985e55_.py b/migrations/versions/ea83ce985e55_.py new file mode 100644 index 00000000..bbf1fba3 --- /dev/null +++ b/migrations/versions/ea83ce985e55_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: ea83ce985e55 +Revises: 16eb610b7751 +Create Date: 2022-11-05 22:09:50.875158 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'ea83ce985e55' +down_revision = '16eb610b7751' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('package_daily_stats', + sa.Column('package_id', sa.Integer(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('platform_minetest', sa.Integer(), nullable=False), + sa.Column('platform_other', sa.Integer(), nullable=False), + sa.Column('reason_new', sa.Integer(), nullable=False), + sa.Column('reason_dependency', sa.Integer(), nullable=False), + sa.Column('reason_update', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), + sa.PrimaryKeyConstraint('package_id', 'date') + ) + + +def downgrade(): + op.drop_table('package_daily_stats')