diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index ab3ad65..15ce684 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -1,14 +1,12 @@ name: Publish Images on: - workflow_run: - workflows: ["Run tests"] - branches: [master] - types: - - completed + push: + tags: + - "v*" env: - REQUIREMENTS: requirements-test.txt + REQUIREMENTS: requirements.txt jobs: build_and_upload_images: diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d15d6..9c99007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### General + +- Add basic analytics for tracking tool launches + +### Bugfixes + +- Trigger Github Actions image push to GHCR on new release tags + ## [6.0.0] - 2026-04-10 ### General diff --git a/lti/migrations/versions/459b2a3ec9e7_.py b/lti/migrations/versions/459b2a3ec9e7_.py new file mode 100644 index 0000000..59eedaa --- /dev/null +++ b/lti/migrations/versions/459b2a3ec9e7_.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: 459b2a3ec9e7 +Revises: ff2a37151e96 +Create Date: 2026-04-22 14:22:27.214234 + +""" + +# revision identifiers, used by Alembic. +revision = "459b2a3ec9e7" +down_revision = "ff2a37151e96" + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "launch_log", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("launch_id", sa.String(length=255), nullable=False), + sa.Column("canvas_course_id", sa.String(length=255), nullable=True), + sa.Column("canvas_course_name", sa.String(length=255), nullable=True), + sa.Column("user_role", sa.String(length=50), nullable=False), + sa.Column("launched_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("launch_log", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("ix_launch_log_launch_id"), ["launch_id"], unique=False + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("launch_log", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_launch_log_launch_id")) + + op.drop_table("launch_log") + # ### end Alembic commands ### diff --git a/lti/models.py b/lti/models.py index 611b6e6..229f565 100644 --- a/lti/models.py +++ b/lti/models.py @@ -85,6 +85,16 @@ def __init__(self, canvas_id, course_id, title=None, time_limit=None): self.time_limit = time_limit +class LaunchLog(db.Model): + __tablename__ = "launch_log" + id = db.Column(db.Integer, primary_key=True) + launch_id = db.Column(db.String(255), nullable=False, index=True) + canvas_course_id = db.Column(db.String(255)) + canvas_course_name = db.Column(db.String(255)) + user_role = db.Column(db.String(50), nullable=False) + launched_at = db.Column(db.DateTime, nullable=False, default=db.func.now()) + + # ============================================ # LTI 1.3 Models # ============================================ diff --git a/lti/views.py b/lti/views.py index c94025a..faaabe5 100644 --- a/lti/views.py +++ b/lti/views.py @@ -3,6 +3,7 @@ import json import logging from collections import defaultdict +from datetime import datetime from logging.config import dictConfig from subprocess import call from urllib.parse import urlparse @@ -28,6 +29,7 @@ from models import ( Course, Extension, + LaunchLog, Quiz, Registration, User, @@ -266,19 +268,41 @@ def launch(): flask_request, tool_conf, launch_data_storage=launch_data_storage ) - session["canvas_email"] = message_launch.get_launch_data().get("email") session["error"] = False - session["roles"] = message_launch.get_launch_data().get( + launch_data = message_launch.get_launch_data() + session["canvas_email"] = launch_data.get("email") + session["roles"] = launch_data.get( "https://purl.imsglobal.org/spec/lti/claim/roles" ) session["launch_id"] = message_launch.get_launch_id() - session["course_id"] = message_launch.get_launch_data()[ + session["course_id"] = launch_data[ "https://purl.imsglobal.org/spec/lti/claim/custom" ]["canvas_course_id"] - session["canvas_user_id"] = message_launch.get_launch_data()[ + session["canvas_user_id"] = launch_data[ "https://purl.imsglobal.org/spec/lti/claim/custom" ]["canvas_user_id"] + context_claim = launch_data.get( + "https://purl.imsglobal.org/spec/lti/claim/context", {} + ) + roles = session.get("roles") or [] + if any("Administrator" in r for r in roles): + user_role = "Administrator" + elif any("Instructor" in r for r in roles): + user_role = "Instructor" + else: + first_role = roles[0] if roles else "" + user_role = first_role.split("#")[-1] + log_entry = LaunchLog( + launch_id=session["launch_id"], + canvas_course_id=session["course_id"], + canvas_course_name=context_claim.get("title"), + user_role=user_role, + launched_at=datetime.utcnow(), + ) + db.session.add(log_entry) + db.session.commit() + # Redirect to the quiz for your course return redirect(url_for("quiz", course_id=session["course_id"]))