Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions migrations/versions/a1b2c3d4e5f6_add_report_log_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Add Report Log Table

Revision ID: a1b2c3d4e5f6
Revises: a62e70d564b3
Create Date: 2024-01-01 00:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'a1b2c3d4e5f6'
down_revision = 'a62e70d564b3'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('report_log',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('date_created', sa.DateTime(), nullable=True),
sa.Column('date_modified', sa.DateTime(), nullable=True),
sa.Column('report_id', sa.Integer(), nullable=False),
sa.Column('subject_id', sa.Integer(), nullable=False),
sa.Column('course_id', sa.Integer(), nullable=True),
sa.Column('assignment_id', sa.Integer(), nullable=True),
sa.Column('event_type', sa.String(length=255), nullable=False),
sa.Column('field', sa.String(length=255), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.Column('client_timestamp', sa.String(length=255), nullable=True),
sa.Column('client_timezone', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['assignment_id'], ['assignment.id'], ),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.ForeignKeyConstraint(['report_id'], ['report.id'], ),
sa.ForeignKeyConstraint(['subject_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('report_log_report_index', 'report_log', ['report_id'], unique=False)
op.create_index('report_log_subject_index', 'report_log', ['subject_id'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('report_log_subject_index', table_name='report_log')
op.drop_index('report_log_report_index', table_name='report_log')
op.drop_table('report_log')
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions models/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class Assignment(EnhancedBase):
sample_submissions: Mapped[list["SampleSubmission"]] = db.relationship(back_populates="assignment")
submission_logs: Mapped[list["SubmissionLog"]] = db.relationship(back_populates="assignment")
assignment_logs: Mapped[list["AssignmentLog"]] = db.relationship(back_populates="assignment")
report_logs: Mapped[list["ReportLog"]] = db.relationship(back_populates="assignment")
submissions: Mapped[list["Submission"]] = db.relationship(back_populates="assignment")
memberships: Mapped[list["AssignmentGroupMembership"]] = db.relationship(back_populates="assignment")
reports: Mapped[list["Report"]] = db.relationship(back_populates="assignment")
Expand Down
1 change: 1 addition & 0 deletions models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Course(Base):
role_logs: Mapped[list["RoleLog"]] = db.relationship(back_populates="course")
assignment_logs: Mapped[list["AssignmentLog"]] = db.relationship(back_populates="course")
submission_logs: Mapped[list["SubmissionLog"]] = db.relationship(back_populates="course")
report_logs: Mapped[list["ReportLog"]] = db.relationship(back_populates="course")
submissions: Mapped[list["Submission"]] = db.relationship(back_populates="course")
invites: Mapped[list["Invite"]] = db.relationship(back_populates="course")
reports: Mapped[list["Report"]] = db.relationship(back_populates="course")
Expand Down
15 changes: 14 additions & 1 deletion models/enums/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,17 @@ class SubmissionLogEvent(StrEnum):
TRANSFER = "transfer"
CANVAS = "canvas"
EXTEND_TIME = "extend_time"
START_TIMER = "start_timer"
START_TIMER = "start_timer"


class ReportLogEvent(StrEnum):
CREATE = "create"
DELETE = "delete"
START = "start"
PROGRESS = "progress"
FINISH = "finish"
ERROR = "error"
VIEW = "view"
DOWNLOAD = "download"
SHARE = "share"
EDIT = "edit"
1 change: 1 addition & 0 deletions models/log_tables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from models.log_tables.role_log import RoleLog
from models.log_tables.assignment_log import AssignmentLog
from models.log_tables.submission_log import SubmissionLog
from models.log_tables.report_log import ReportLog
68 changes: 68 additions & 0 deletions models/log_tables/report_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import logging
from collections import OrderedDict
import time
import json
from datetime import datetime, timedelta
from typing import Optional

from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Column, String, Integer, ForeignKey, Text, func, JSON, Index, and_, Enum

import models
from models.generics.models import db, ma
from models.generics.base import Base
from common.dates import datetime_to_string, string_to_datetime
from models.enums import ReportLogEvent
from common.databases import get_enum_values
import models


class ReportLog(Base):
__tablename__ = "report_log"
# Identification
report_id: Mapped[int] = mapped_column(Integer(), ForeignKey('report.id'))
subject_id: Mapped[int] = mapped_column(Integer(), ForeignKey('user.id'))
# Optional context - may be derived from report
course_id: Mapped[Optional[int]] = mapped_column(Integer(), ForeignKey('course.id'), nullable=True)
assignment_id: Mapped[Optional[int]] = mapped_column(Integer(), ForeignKey('assignment.id'), nullable=True)
# Actual event data
event_type: Mapped[ReportLogEvent] = mapped_column(Enum(ReportLogEvent, values_callable=get_enum_values), nullable=False)
# For events that need additional data
field: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
value: Mapped[Optional[str]] = mapped_column(Text(), nullable=True)
# When the client initiated this action
client_timestamp: Mapped[Optional[str]] = mapped_column(String(255), default="", nullable=True)
client_timezone: Mapped[Optional[str]] = mapped_column(String(255), default="", nullable=True)

report: Mapped["Report"] = db.relationship(back_populates="report_logs")
subject: Mapped["User"] = db.relationship(back_populates="report_logs")
course: Mapped[Optional["Course"]] = db.relationship(back_populates="report_logs")
assignment: Mapped[Optional["Assignment"]] = db.relationship(back_populates="report_logs")

__table_args__ = (Index('report_log_report_index', "report_id"),
Index('report_log_subject_index', "subject_id"))

@staticmethod
def new(report_id: int, subject_id: int, event_type: ReportLogEvent,
course_id: int = None, assignment_id: int = None,
field: str = None, value: str = None,
client_timestamp: str = None, client_timezone: str = None):
# Validate the event
if not isinstance(event_type, ReportLogEvent):
raise ValueError(f"Invalid event type: {event_type}")
if event_type == ReportLogEvent.EDIT and (field is None or value is None):
raise ValueError("Field and value must be provided for edit events")
if event_type == ReportLogEvent.EDIT and not hasattr(models.Report, field):
raise ValueError(f"Unknown field name for Report: {field}")
# Create the log
log = ReportLog(report_id=report_id, subject_id=subject_id,
course_id=course_id, assignment_id=assignment_id,
event_type=event_type,
field=field, value=value,
client_timestamp=client_timestamp, client_timezone=client_timezone)
db.session.add(log)
db.session.commit()
return log

def __str__(self):
return f'<ReportLog {self.event_type} event for {self.report_id}>'
1 change: 1 addition & 0 deletions models/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Report(Base):
assignment: Mapped["Assignment"] = db.relationship(back_populates="reports")
owner: Mapped["User"] = db.relationship(back_populates="reports")
course: Mapped["Course"] = db.relationship(back_populates="reports")
report_logs: Mapped[list["ReportLog"]] = db.relationship(back_populates="report")

def encode_json(self):
return {
Expand Down
1 change: 1 addition & 0 deletions models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class User(Base, UserMixin):
foreign_keys="RoleLog.authorizer_id")
assignment_logs: Mapped[list["AssignmentLog"]] = db.relationship(back_populates="subject")
submission_logs: Mapped[list["SubmissionLog"]] = db.relationship(back_populates="subject")
report_logs: Mapped[list["ReportLog"]] = db.relationship(back_populates="subject")
reviews: Mapped[list["Review"]] = db.relationship(back_populates="author")
submissions: Mapped[list["Submission"]] = db.relationship(back_populates="user")
reports: Mapped[list["Report"]] = db.relationship(back_populates="owner")
Expand Down