elmeg-demo/backend/models_tickets.py
fullsizemalt 14a509ddb5
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat: Add bug tracker MVP (decoupled, feature-flagged)
- Backend: Ticket and TicketComment models (no FK to User)
- API: /tickets/* endpoints for submit, view, comment, upvote
- Admin: /tickets/admin/* for triage queue
- Frontend: /bugs pages (submit, my-tickets, known-issues, detail)
- Feature flag: ENABLE_BUG_TRACKER env var (default: true)

To disable: Set ENABLE_BUG_TRACKER=false
To remove: Delete models_tickets.py, routers/tickets.py, frontend/app/bugs/
2025-12-23 13:18:00 -08:00

140 lines
3.8 KiB
Python

"""
Bug Tracker Models - ISOLATED MODULE
No dependencies on main Elmeg models.
Can be removed by: deleting this file + routes file + removing router import from main.py
"""
from datetime import datetime
from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship
from enum import Enum
class TicketType(str, Enum):
BUG = "bug"
FEATURE = "feature"
QUESTION = "question"
OTHER = "other"
class TicketStatus(str, Enum):
OPEN = "open"
IN_PROGRESS = "in_progress"
RESOLVED = "resolved"
CLOSED = "closed"
class TicketPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class Ticket(SQLModel, table=True):
"""
Support ticket - fully decoupled from User model.
Stores reporter info as strings, not FKs.
"""
id: Optional[int] = Field(default=None, primary_key=True)
ticket_number: str = Field(unique=True, index=True) # ELM-001
type: TicketType = Field(default=TicketType.BUG)
status: TicketStatus = Field(default=TicketStatus.OPEN)
priority: TicketPriority = Field(default=TicketPriority.MEDIUM)
title: str = Field(max_length=200)
description: str = Field(default="")
# Reporter info - stored as strings, not FK
reporter_email: str = Field(index=True)
reporter_name: Optional[str] = None
reporter_user_id: Optional[int] = None # Reference only, not FK
# Assignment - stored as strings
assigned_to_email: Optional[str] = None
assigned_to_name: Optional[str] = None
is_public: bool = Field(default=False)
upvotes: int = Field(default=0)
# Environment info
browser: Optional[str] = None
os: Optional[str] = None
page_url: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
resolved_at: Optional[datetime] = None
# Relationships (within ticket system only)
comments: List["TicketComment"] = Relationship(back_populates="ticket")
class TicketComment(SQLModel, table=True):
"""Comment on a ticket - no FK to User"""
id: Optional[int] = Field(default=None, primary_key=True)
ticket_id: int = Field(foreign_key="ticket.id")
# Author info - stored as strings
author_email: str
author_name: str
author_user_id: Optional[int] = None # Reference only
content: str
is_internal: bool = Field(default=False) # Admin-only visibility
created_at: datetime = Field(default_factory=datetime.utcnow)
# Relationship
ticket: Optional[Ticket] = Relationship(back_populates="comments")
# ============ Schemas ============
class TicketCreate(SQLModel):
type: TicketType = TicketType.BUG
priority: TicketPriority = TicketPriority.MEDIUM
title: str
description: str = ""
reporter_email: Optional[str] = None
reporter_name: Optional[str] = None
browser: Optional[str] = None
os: Optional[str] = None
page_url: Optional[str] = None
class TicketUpdate(SQLModel):
status: Optional[TicketStatus] = None
priority: Optional[TicketPriority] = None
assigned_to_email: Optional[str] = None
assigned_to_name: Optional[str] = None
is_public: Optional[bool] = None
class TicketCommentCreate(SQLModel):
content: str
class TicketRead(SQLModel):
id: int
ticket_number: str
type: TicketType
status: TicketStatus
priority: TicketPriority
title: str
description: str
reporter_email: str
reporter_name: Optional[str]
is_public: bool
upvotes: int
created_at: datetime
updated_at: datetime
resolved_at: Optional[datetime]
class TicketCommentRead(SQLModel):
id: int
author_name: str
content: str
is_internal: bool
created_at: datetime