Skip to content
Merged
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
72 changes: 72 additions & 0 deletions backend/alembic/versions/cb863878e0d1_add_contest_report_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""add_contest_report_table

Revision ID: xxxxxxxxxxxx # Alembic automatically generates this
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no revision ID, this introduces a breaking change.

Copy link
Collaborator Author

@GauriGupta21 GauriGupta21 Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder comment was present in the initial draft of the migration file, will change it

Revises: yyyyyyyyyyyy # Previous migration ID
Create Date: 2026-01-21 10:30:00.000000

"""
from alembic import op
import sqlalchemy as sa


revision = 'cb863878e0d1'
down_revision = 'de4074ff4ff8'
branch_labels = None
depends_on = None


def upgrade():
"""Create contest_reports table"""

# Create contest_reports table
op.create_table(
'contest_reports',

# Primary key
sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),

# Foreign keys
sa.Column('contest_id', sa.Integer(), nullable=False),
sa.Column('generated_by', sa.Integer(), nullable=False),

# Report configuration
sa.Column('report_type', sa.String(length=20), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'),

# File storage
sa.Column('file_path', sa.String(length=500), nullable=True),

# Error handling
sa.Column('error_message', sa.Text(), nullable=True),

# Report parameters (JSON)
sa.Column('report_metadata', sa.Text(), nullable=True),

# Timestamps
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()),

# Primary key constraint
sa.PrimaryKeyConstraint('id'),

# Foreign key constraints
sa.ForeignKeyConstraint(['contest_id'], ['contests.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['generated_by'], ['users.id'], ondelete='CASCADE'),
)

# Create indexes for better query performance
op.create_index('idx_contest_reports_contest_id', 'contest_reports', ['contest_id'])
op.create_index('idx_contest_reports_generated_by', 'contest_reports', ['generated_by'])
op.create_index('idx_contest_reports_status', 'contest_reports', ['status'])


def downgrade():
"""Drop contest_reports table"""

# Drop indexes first
op.drop_index('idx_contest_reports_status', table_name='contest_reports')
op.drop_index('idx_contest_reports_generated_by', table_name='contest_reports')
op.drop_index('idx_contest_reports_contest_id', table_name='contest_reports')

# Drop table
op.drop_table('contest_reports')
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""merge report feature and outreach dashboard migrations

Revision ID: e4e56960f418
Revises: cb863878e0d1, d55c876a1323
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is 2 revision ID, this introduces a breaking change.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The revision ID issue was fixed in the final commit.
The merge migration correctly resolves two heads into one, as per Alembic standards.

Create Date: 2026-01-30 20:30:29.797250

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'e4e56960f418'
down_revision = ('cb863878e0d1', 'd55c876a1323')
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass

3 changes: 3 additions & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@
from app.models.user import User # pylint: disable=unused-import
from app.models.contest import Contest # pylint: disable=unused-import
from app.models.submission import Submission # pylint: disable=unused-import
from app.models.contest_report import ContestReport # pylint: disable=unused-import
from app.routes.user_routes import user_bp
from app.routes.contest_routes import contest_bp
from app.routes.submission_routes import submission_bp
from app.routes.report_routes import report_bp
from app.utils import (
extract_page_title_from_url,
build_mediawiki_revisions_api_params,
Expand Down Expand Up @@ -201,6 +203,7 @@ def create_app():
app.register_blueprint(user_bp, url_prefix='/api/user') # User management endpoints
app.register_blueprint(contest_bp, url_prefix='/api/contest') # Contest endpoints
app.register_blueprint(submission_bp, url_prefix='/api/submission') # Submission endpoints
app.register_blueprint(report_bp, url_prefix='/api/report')


# ---------------------------------------------------------------------------
Expand Down
135 changes: 135 additions & 0 deletions backend/app/models/contest_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Contest Report Model for WikiContest Application
Stores generated contest reports and their metadata
FIXED: Proper inheritance from BaseModel for created_at/updated_at
"""

from datetime import datetime
from app.database import db
from app.models.base_model import BaseModel


class ContestReport(BaseModel):
"""
Contest Report model representing generated reports for contests

Attributes:
id: Primary key, auto-incrementing integer
contest_id: Foreign key to contests table
report_type: Type of report ('csv' or 'pdf')
status: Generation status ('pending', 'processing', 'completed', 'failed')
file_path: Path to generated report file
generated_by: Foreign key to users table (who requested the report)
error_message: Error message if generation failed
report_metadata: JSON containing report parameters (top_n, filters, etc.)
created_at: Timestamp when report generation was requested (from BaseModel)
updated_at: Timestamp when report status was last updated (from BaseModel)
"""

__tablename__ = "contest_reports"

# Primary key
id = db.Column(db.Integer, primary_key=True, autoincrement=True)

# Foreign keys
contest_id = db.Column(db.Integer, db.ForeignKey("contests.id"), nullable=False)
generated_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)

# Report configuration
report_type = db.Column(db.String(20), nullable=False) # 'csv' or 'pdf'
status = db.Column(db.String(20), default='pending', nullable=False)

# File storage
file_path = db.Column(db.String(500), nullable=True)

# Error handling
error_message = db.Column(db.Text, nullable=True)

# Report parameters (stored as JSON string)
report_metadata = db.Column(db.Text, nullable=True)

# Relationships
contest = db.relationship("Contest", backref="reports")
generator = db.relationship("User", backref="generated_reports")

def __init__(self, contest_id, report_type, generated_by, report_metadata=None):
"""
Initialize a new ContestReport instance

IMPORTANT: Don't manually set created_at/updated_at
They are automatically handled by BaseModel

Args:
contest_id: ID of the contest
report_type: Type of report ('csv' or 'pdf')
generated_by: ID of user requesting the report
report_metadata: Optional dict of report parameters
"""
# Call parent __init__ to set created_at/updated_at
super().__init__()

self.contest_id = contest_id
self.report_type = report_type
self.generated_by = generated_by
self.status = 'pending'

# Store report_metadata as JSON string
if report_metadata:
import json
self.report_metadata = json.dumps(report_metadata)
else:
self.report_metadata = None

def get_metadata(self):
"""
Get report metadata as dictionary

Returns:
dict or None: Report parameters
"""
if not self.report_metadata:
return None
try:
import json
return json.loads(self.report_metadata)
except Exception:
return None

def is_completed(self):
"""Check if report generation is completed"""
return self.status == 'completed'

def is_failed(self):
"""Check if report generation failed"""
return self.status == 'failed'

def is_processing(self):
"""Check if report is currently being generated"""
return self.status == 'processing'

def to_dict(self):
"""
Convert report instance to dictionary for JSON serialization

NOTE: created_at and updated_at come from BaseModel

Returns:
dict: Report data
"""
return {
'id': self.id,
'contest_id': self.contest_id,
'report_type': self.report_type,
'status': self.status,
'file_path': self.file_path if self.is_completed() else None,
'error_message': self.error_message if self.is_failed() else None,
'report_metadata': self.get_metadata(),
'generated_by': self.generated_by,
# ✅ These come from BaseModel - should work now
'created_at': self.created_at.isoformat() if hasattr(self, 'created_at') and self.created_at else None,
'updated_at': self.updated_at.isoformat() if hasattr(self, 'updated_at') and self.updated_at else None,
}

def __repr__(self):
"""String representation of ContestReport instance"""
return f"<ContestReport {self.id}: {self.report_type} for Contest {self.contest_id}>"
Loading
Loading