-
Notifications
You must be signed in to change notification settings - Fork 8
Add Manage Summary Report feature with backend and frontend changes #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is 2 revision ID, this introduces a breaking change.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The revision ID issue was fixed in the final commit. |
||
| 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 | ||
|
|
||
| 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}>" |
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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