refactor: redesign admin permalinks to RESTful paths
- Replace fragile /admin/ai-playground and /admin/ai-generation routes
with item-scoped /admin/questions/{id}/generate endpoints
- Add /admin/tryouts/{id}/questions route using Tryout primary key
instead of composite query params (?tryout_id=X&website_id=Y)
- Fix variant detail and review-bulk endpoints to use scoped paths
- Update all internal links (dashboard, hierarchy, exams) to new routes
- Remove obsolete ai_playground_view/submit/save functions
This commit is contained in:
700
ADMIN_UI_REDESIGN_PLAN.md
Normal file
700
ADMIN_UI_REDESIGN_PLAN.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# Admin UI Redesign Plan
|
||||
|
||||
> **Document Type:** UI/UX Improvement Plan
|
||||
> **Current System:** IRT Bank Soal Admin
|
||||
> **Date:** 2026-06-15
|
||||
> **Status:** Draft for Review
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The current admin interface is built from a **developer/system perspective** rather than a **human/admin perspective**. This plan outlines a complete redesign to make the admin dashboard intuitive, workflow-oriented, and human-readable.
|
||||
|
||||
### Current Problems
|
||||
|
||||
| Problem | Impact |
|
||||
|---------|--------|
|
||||
| Navigation uses technical terms | Admins don't understand menu labels |
|
||||
| Multiple unrelated features in one view | Confusing, overwhelming |
|
||||
| Data displayed in database terminology | Hard to interpret scores |
|
||||
| No clear workflow guidance | Admin doesn't know what to do first |
|
||||
| No contextual help | Unclear what each feature does |
|
||||
| Mixed concern pages | AI + Questions + Calibration all on one page |
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Current Navigation (System POV)
|
||||
|
||||
```
|
||||
├── Dashboard (raw counts)
|
||||
├── Websites (technical list)
|
||||
├── Tryout Import (system term)
|
||||
├── Data Hierarchy (developer term)
|
||||
├── Basis Items (technical term)
|
||||
├── Calibration Status (technical term)
|
||||
├── Item Statistics (technical term)
|
||||
├── Session Overview (technical term)
|
||||
├── AI Playground (slang)
|
||||
└── Password Info (unrelated)
|
||||
```
|
||||
|
||||
### Current Issues
|
||||
|
||||
1. **Naming Problems:**
|
||||
- "Basis Items" → Should be "Question Templates" or "Original Questions"
|
||||
- "Data Hierarchy" → Should be "Data Overview" or "Website Structure"
|
||||
- "Tryout Import" → Should be "Import Questions"
|
||||
- "Calibration Status" → Should be "Question Quality" or "Difficulty Analysis"
|
||||
- "AI Playground" → Should be "Generate AI Questions"
|
||||
- "Session Overview" → Should be "Student Attempts"
|
||||
|
||||
2. **Dashboard Issues:**
|
||||
- Shows raw database counts (Tryouts, Items, Sessions)
|
||||
- No meaningful KPIs or actionable insights
|
||||
- No visual indicators of system health
|
||||
|
||||
3. **Page Organization:**
|
||||
- Too many technical terms on each page
|
||||
- Tables show raw data without explanation
|
||||
- No breadcrumbs or context
|
||||
|
||||
---
|
||||
|
||||
## Proposed Redesign
|
||||
|
||||
### New Navigation Structure (Human POV)
|
||||
|
||||
```
|
||||
🎯 Dashboard (Home)
|
||||
├── System Health Summary
|
||||
├── Quick Actions
|
||||
└── Recent Activity
|
||||
|
||||
📋 Questions Bank
|
||||
├── All Questions (list + search)
|
||||
├── Question Templates (basis items)
|
||||
├── Import Questions (from Excel/JSON)
|
||||
└── Question Quality (calibration status)
|
||||
|
||||
🤖 AI Generation
|
||||
├── Generate New Questions
|
||||
├── Review Generated Questions
|
||||
└── Generation History
|
||||
|
||||
📊 Exams (Tryouts)
|
||||
├── All Exams (list)
|
||||
├── Exam Settings (scoring mode)
|
||||
├── Student Attempts
|
||||
└── Normalization Settings
|
||||
|
||||
📈 Reports
|
||||
├── Student Performance Report
|
||||
├── Item Analysis Report
|
||||
├── Exam Comparison Report
|
||||
└── Scheduled Reports
|
||||
|
||||
⚙️ Settings
|
||||
├── Websites Management
|
||||
├── Account Settings
|
||||
└── System Info
|
||||
```
|
||||
|
||||
### Navigation Mapping Table
|
||||
|
||||
| Current Menu | New Menu | Reason |
|
||||
|-------------|----------|--------|
|
||||
| Dashboard | 🎯 Dashboard | Home base |
|
||||
| Websites | ⚙️ Settings > Websites | Configuration |
|
||||
| Tryout Import | 📋 Questions > Import Questions | Workflow step |
|
||||
| Data Hierarchy | ⚙️ Settings | Admin settings |
|
||||
| Basis Items | 📋 Questions > Question Templates | Content management |
|
||||
| Calibration Status | 📋 Questions > Question Quality | Quality assurance |
|
||||
| Item Statistics | 📈 Reports > Item Analysis | Reporting |
|
||||
| Session Overview | 📊 Exams > Student Attempts | Workflow |
|
||||
| AI Playground | 🤖 AI Generation | Dedicated feature |
|
||||
| Password Info | ⚙️ Settings > Account | Configuration |
|
||||
|
||||
---
|
||||
|
||||
## Detailed Page Redesigns
|
||||
|
||||
### 1. Dashboard (Home) — `GET /admin/dashboard`
|
||||
|
||||
**Current State:**
|
||||
```python
|
||||
# Shows raw counts
|
||||
body = f"""
|
||||
<p>Signed in as <strong>{admin}</strong>.</p>
|
||||
<div class="grid">
|
||||
<div class="stat">Tryouts<strong>{tryouts}</strong></div>
|
||||
<div class="stat">Items<strong>{items}</strong></div>
|
||||
<div class="stat">Sessions<strong>{sessions}</strong></div>
|
||||
<div class="stat">Completed Sessions<strong>{completed}</strong></div>
|
||||
</div>
|
||||
<p><a href="/admin/ai-playground">Open AI Playground</a></p>
|
||||
"""
|
||||
```
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Good Morning, Admin! 👋 │
|
||||
│ Last login: Today at 9:00 AM │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📊 System Overview │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 5 Exams │ │ 450 │ │ 1,234 │ │ 89% │ │
|
||||
│ │ Active │ │ Questions│ │ Students │ │ Avg Score│ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ ⚠️ Attention Needed │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚡ 23 questions need calibration (do this first!) │ │
|
||||
│ │ 📝 5 AI-generated questions pending review │ │
|
||||
│ │ 📥 2 exam exports ready for download │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🚀 Quick Actions │
|
||||
│ [Import Questions] [Generate AI] [View Reports] [Add Exam] │
|
||||
│ │
|
||||
│ 📈 Recent Activity │
|
||||
│ • 12 students completed "UTBK 2024" in last hour │
|
||||
│ • 3 new questions generated via AI │
|
||||
│ • Calibration completed for "SIMAK UI" (95% ready) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Human-readable greeting with time
|
||||
- Meaningful metrics (not raw counts)
|
||||
- Actionable alerts with urgency indicators
|
||||
- Quick action buttons with clear labels
|
||||
- Recent activity feed
|
||||
|
||||
---
|
||||
|
||||
### 2. Questions Bank — Questions List (`/admin/questions`)
|
||||
|
||||
**Current State:** Table with raw database fields
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📋 Question Bank │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [Search: "matematika" ] [Filter ▼] [🔍] │
|
||||
│ │
|
||||
│ Showing 450 questions across 5 exams │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☐ │ Q1 │ Berapakah hasil dari 2 + 2? │ │
|
||||
│ │ │ │ ▸ Easy (p=0.85) | Used 234x | SIMAK UI │ │
|
||||
│ ├────┼─────┼──────────────────────────────────────────────┤ │
|
||||
│ │ ☐ │ Q5 │ Hitung integral dari x² dx... │ │
|
||||
│ │ │ │ ▸ Medium (p=0.45) | Used 89x | UTBK 2024 │ │
|
||||
│ ├────┼─────┼──────────────────────────────────────────────┤ │
|
||||
│ │ ☐ │ Q12 │ Jelaskan teori evolusi... │ │
|
||||
│ │ │ │ ▸ Hard (p=0.22) | Used 45x | ONM 2024 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Delete Selected] [Export Selected] [Edit Selected] │
|
||||
│ │
|
||||
│ 📄 Page 1 of 23 [<] [1] [2] [3] ... [>] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Question preview in list (not just ID)
|
||||
- Human-readable difficulty (Easy/Medium/Hard)
|
||||
- Usage count (how many times used)
|
||||
- Which exam it belongs to
|
||||
- Visual indicators for difficulty colors
|
||||
|
||||
---
|
||||
|
||||
### 3. Question Templates — (`/admin/templates`)
|
||||
|
||||
**Current State:** "Basis Items" - confusing technical term
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Question Templates │
|
||||
│ (Original questions used to generate AI variants) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Templates are your "master questions" that AI uses to │
|
||||
│ create different versions with varying difficulty levels. │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📝 Template #45: "Berapakah hasil dari 2 + 2?" │ │
|
||||
│ │ AI Generated Variants: 12 (3 easy, 6 medium, 3 hard) │ │
|
||||
│ │ [View All Variants] [Generate More] [Edit] │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ 📝 Template #89: "Hitung integral dari x² dx..." │ │
|
||||
│ │ AI Generated Variants: 8 (2 easy, 4 medium, 2 hard) │ │
|
||||
│ │ [View All Variants] [Generate More] [Edit] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [+ Create New Template] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Clear explanation of what templates are
|
||||
- Visual representation of variants
|
||||
- Easy action buttons
|
||||
- "Create New Template" prominent
|
||||
|
||||
---
|
||||
|
||||
### 4. AI Generation — (`/admin/ai-generation`)
|
||||
|
||||
**Current State:** "AI Playground" - informal, confusing tabs
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🤖 AI Question Generator │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Generate new question variants using AI. │
|
||||
│ Select a template question and specify difficulty level. │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ 📝 Select Template │ │ 🎯 Target Difficulty │ │
|
||||
│ │ [Dropdown: Questions]│ │ ○ Easy (p > 0.70) │ │
|
||||
│ └──────────────────────┘ │ ● Medium (p ≈ 0.50) │ │
|
||||
│ │ ○ Hard (p < 0.30) │ │
|
||||
│ ┌──────────────────────┐ └──────────────────────┘ │
|
||||
│ │ 📝 How many variants?│ │
|
||||
│ │ [1] [3] [5] [10] │ ┌──────────────────────┐ │
|
||||
│ └──────────────────────┘ │ 💬 Additional Notes │ │
|
||||
│ │ [Optional context...] │ │
|
||||
│ └──────────────────────┘ │
|
||||
│ │
|
||||
│ [🚀 Generate Questions] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 📋 Generated Questions (Pending Review) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔄 Generating... 2 of 5 questions completed │ │
|
||||
│ │ [████████░░] 60% │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ✅ Generated & Ready for Review: │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✓ Variant #123: "Berapakah hasil dari 3 + 4?" (Easy) │ │
|
||||
│ │ [Preview] [Approve] [Regenerate] [Reject] │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ ✓ Variant #124: "Hitung hasil dari 5 + 6..." (Easy) │ │
|
||||
│ │ [Preview] [Approve] [Regenerate] [Reject] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Clear, labeled sections
|
||||
- Radio buttons for difficulty (not dropdown)
|
||||
- Progress indicator during generation
|
||||
- Clear action buttons (Approve/Reject/Regenerate)
|
||||
- Explanation of what each option means
|
||||
|
||||
---
|
||||
|
||||
### 5. Question Quality (Calibration) — (`/admin/question-quality`)
|
||||
|
||||
**Current State:** "Calibration Status" - technical IRT terminology
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 Question Quality Dashboard │
|
||||
│ (Shows how well each question is "calibrated" for testing) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📖 What is Question Quality? │
|
||||
│ Questions become "calibrated" after many students answer them. │
|
||||
│ Well-calibrated questions give accurate student scores. │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Overall Quality: ████████░░ 78% │
|
||||
│ (78 out of 100 questions are ready for adaptive testing) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📋 By Exam │ │
|
||||
│ │ │ │
|
||||
│ │ UTBK 2024 ████████████ 95% ✓ Ready │ │
|
||||
│ │ SIMAK UI █████████░░░ 72% ⚠️ Partial │ │
|
||||
│ │ ONM 2024 ██████░░░░░░ 45% ❌ Needs more data│ │
|
||||
│ │ PASIAD Selection ████████████ 100% ✓ Excellent │ │
|
||||
│ │ │ │
|
||||
│ │ [Run Calibration for All Exams] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Questions Needing Attention: │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚠️ Q45 - "Hitung integral..." only answered 12 times │ │
|
||||
│ │ Need at least 100 answers to calibrate properly. │ │
|
||||
│ │ Current estimate: p=0.42 (might change) │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ ❌ Q78 - "Teori relativitas..." has conflicting answers │ │
|
||||
│ │ Check if correct answer is correct in database. │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Clear explanation of what calibration means
|
||||
- Progress bars for visual understanding
|
||||
- Status indicators (✓ Ready, ⚠️ Partial, ❌ Needs data)
|
||||
- Specific recommendations for action
|
||||
- User-friendly difficulty explanation
|
||||
|
||||
---
|
||||
|
||||
### 6. Student Attempts — (`/admin/student-attempts`)
|
||||
|
||||
**Current State:** "Session Overview" - raw database table
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 Student Attempts │
|
||||
│ (See how students performed on each exam) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Filter: [Select Exam ▼] [Status ▼] [Date Range ▼] [Search] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📋 UTBK 2024 Results │ │
|
||||
│ │ │ │
|
||||
│ │ Participants: 1,234 students │ │
|
||||
│ │ Average Score (NM): 672 / 1000 │ │
|
||||
│ │ Average Score (NN): 505 / 1000 │ │
|
||||
│ │ Completion Rate: 98% (1,209 completed) │ │
|
||||
│ │ │ │
|
||||
│ │ Score Distribution: │ │
|
||||
│ │ ▁▂▃▇█▇▃▂▁ (bell curve centered around 500) │ │
|
||||
│ │ 200 300 400 500 600 700 800 900 1000 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Recent Attempts: │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 👤 John Doe (john@example.com) │ │
|
||||
│ │ Exam: UTBK 2024 | Completed: Today, 2:30 PM │ │
|
||||
│ │ Score: NM=720 (85th percentile) | NN=645 │ │
|
||||
│ │ Correct: 28/30 | Time: 45 minutes │ │
|
||||
│ ├─────────────────────────────────────────────────────────┤ │
|
||||
│ │ 👤 Jane Smith (jane@example.com) │ │
|
||||
│ │ Exam: SIMAK UI | Completed: Today, 1:15 PM │ │
|
||||
│ │ Score: NM=580 (45th percentile) | NN=485 │ │
|
||||
│ │ Correct: 22/30 | Time: 52 minutes │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Export All Results] [View Detailed Report] [Schedule Report] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Grouped by exam with summary stats
|
||||
- Human-readable student info
|
||||
- Percentile ranking
|
||||
- Score distribution visualization
|
||||
- Clear action buttons
|
||||
|
||||
---
|
||||
|
||||
### 7. Reports — (`/admin/reports`)
|
||||
|
||||
**Proposed State:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📈 Reports │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Generate detailed analysis reports for exams and students. │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 📊 Student Performance │ │ 📋 Item Analysis │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ See individual student │ │ Analyze question │ │
|
||||
│ │ scores, rankings, and │ │ difficulty, validity, │ │
|
||||
│ │ detailed breakdowns. │ │ and discrimination. │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ [Generate Report] │ │ [Generate Report] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 📈 Exam Comparison │ │ 📅 Scheduled Reports │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Compare scores across │ │ Set up automatic │ │
|
||||
│ │ different exams or │ │ weekly/monthly reports │ │
|
||||
│ │ time periods. │ │ delivery. │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ [Generate Report] │ │ [Manage Schedules] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
- Card-based layout with icons
|
||||
- Clear description of each report type
|
||||
- Visual cards instead of dropdowns
|
||||
- Scheduled reports as first-class feature
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Navigation Redesign (Foundation)
|
||||
|
||||
**Files to modify:**
|
||||
- `app/admin_web.py` - Update `ADMIN_NAV_ITEMS`
|
||||
- Create new route handlers
|
||||
|
||||
**Steps:**
|
||||
1. Rename navigation items with human labels
|
||||
2. Create new route structure
|
||||
3. Implement breadcrumb system
|
||||
4. Add help tooltips
|
||||
|
||||
**New Navigation Structure:**
|
||||
```python
|
||||
ADMIN_NAV_ITEMS = (
|
||||
("Dashboard", "/admin/dashboard", ("/admin/dashboard",)),
|
||||
("Questions", "/admin/questions", ("/admin/questions", "/admin/templates")),
|
||||
("AI Generator", "/admin/ai-generation", ("/admin/ai-generation",)),
|
||||
("Exams", "/admin/exams", ("/admin/exams", "/admin/student-attempts")),
|
||||
("Reports", "/admin/reports", ("/admin/reports",)),
|
||||
("Settings", "/admin/settings", ("/admin/settings",)),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Dashboard Overhaul
|
||||
|
||||
**New Dashboard Components:**
|
||||
1. Greeting with user name and time
|
||||
2. System health cards (with meaningful metrics)
|
||||
3. Action alerts section
|
||||
4. Quick action buttons
|
||||
5. Recent activity feed
|
||||
|
||||
**Files to modify:**
|
||||
- `dashboard_view()` function
|
||||
- `_render_admin_page()` for dashboard-specific layout
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Questions Section
|
||||
|
||||
**New Pages:**
|
||||
1. `/admin/questions` - List all questions with search/filter
|
||||
2. `/admin/questions/{id}` - Question detail view
|
||||
3. `/admin/templates` - Question templates (formerly basis items)
|
||||
4. `/admin/questions/import` - Import wizard
|
||||
|
||||
**Key UI Components:**
|
||||
- Question preview cards
|
||||
- Difficulty badges (Easy/Medium/Hard)
|
||||
- Color-coded indicators
|
||||
- Inline search
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: AI Generation Section
|
||||
|
||||
**New Pages:**
|
||||
1. `/admin/ai-generation` - Main generation interface
|
||||
2. `/admin/ai-generation/review` - Review pending variants
|
||||
3. `/admin/ai-generation/history` - Generation history
|
||||
|
||||
**Key UI Components:**
|
||||
- Template selector with preview
|
||||
- Difficulty radio buttons
|
||||
- Generation progress bar
|
||||
- Batch approve/reject actions
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Exams Section
|
||||
|
||||
**New Pages:**
|
||||
1. `/admin/exams` - List all exams
|
||||
2. `/admin/exams/{id}/settings` - Exam configuration
|
||||
3. `/admin/student-attempts` - Student attempts list
|
||||
4. `/admin/normalization` - Normalization settings
|
||||
|
||||
**Key UI Components:**
|
||||
- Exam cards with status indicators
|
||||
- Student attempt cards
|
||||
- Score distribution visualization
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Reports Section
|
||||
|
||||
**New Pages:**
|
||||
1. `/admin/reports` - Report dashboard
|
||||
2. `/admin/reports/student-performance` - Student report
|
||||
3. `/admin/reports/item-analysis` - Item report
|
||||
4. `/admin/reports/exam-comparison` - Comparison report
|
||||
5. `/admin/reports/scheduled` - Scheduled reports
|
||||
|
||||
**Key UI Components:**
|
||||
- Report type cards
|
||||
- Export format options
|
||||
- Schedule configuration
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation Notes
|
||||
|
||||
### CSS Class Naming Convention
|
||||
|
||||
```css
|
||||
/* Old: System POV */
|
||||
.stat { }
|
||||
.grid { }
|
||||
.table-wrap { }
|
||||
|
||||
/* New: Human POV */
|
||||
.dashboard-hero { }
|
||||
.metric-card { }
|
||||
.question-list { }
|
||||
.difficulty-badge { }
|
||||
.difficulty-easy { background: #dcfce7; }
|
||||
.difficulty-medium { background: #fef3c7; }
|
||||
.difficulty-hard { background: #fee2e2; }
|
||||
```
|
||||
|
||||
### Helper Functions to Create
|
||||
|
||||
```python
|
||||
# In admin_web.py
|
||||
|
||||
def _render_question_card(item: Item) -> str:
|
||||
"""Render a human-readable question card."""
|
||||
difficulty = _human_difficulty(item.ctt_p)
|
||||
difficulty_color = _difficulty_color(item.ctt_p)
|
||||
return f"""
|
||||
<div class="question-card">
|
||||
<div class="difficulty-badge {difficulty_color}">{difficulty}</div>
|
||||
<div class="question-stem">{escape(item.stem[:100])}...</div>
|
||||
<div class="question-meta">
|
||||
Used {item.calibration_sample_size}x |
|
||||
{item.tryout_id}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _human_difficulty(p_value: float | None) -> str:
|
||||
"""Convert p-value to human-readable difficulty."""
|
||||
if p_value is None:
|
||||
return "Unknown"
|
||||
if p_value > 0.70:
|
||||
return "Easy"
|
||||
elif p_value >= 0.30:
|
||||
return "Medium"
|
||||
else:
|
||||
return "Hard"
|
||||
|
||||
def _difficulty_color(p_value: float | None) -> str:
|
||||
"""Get color class for difficulty badge."""
|
||||
if p_value is None:
|
||||
return "difficulty-unknown"
|
||||
if p_value > 0.70:
|
||||
return "difficulty-easy"
|
||||
elif p_value >= 0.30:
|
||||
return "difficulty-medium"
|
||||
else:
|
||||
return "difficulty-hard"
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
```css
|
||||
/* Mobile-friendly layout */
|
||||
@media (max-width: 768px) {
|
||||
.admin-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Time to complete common task | Reduce by 50% |
|
||||
| Admin confusion score | < 2/5 |
|
||||
| Support tickets about UI | Reduce by 80% |
|
||||
| Feature discovery rate | > 90% can find features |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Terminology Mapping
|
||||
|
||||
| System Term | Human Term |
|
||||
|------------|------------|
|
||||
| Tryout | Exam / Test |
|
||||
| Item | Question |
|
||||
| Basis Item | Question Template / Original Question |
|
||||
| Session | Student Attempt |
|
||||
| Calibration | Question Quality / Difficulty Analysis |
|
||||
| IRT | Adaptive Scoring |
|
||||
| CTT | Standard Scoring |
|
||||
| Bobot | Weight / Point Value |
|
||||
| NM | Raw Score |
|
||||
| NN | Normalized Score |
|
||||
| p-value | Difficulty Score |
|
||||
| Theta | Student Ability Score |
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `app/admin_web.py` | Complete UI rewrite |
|
||||
| `app/admin.py` | May need minor updates |
|
||||
| `requirements.txt` | Add any new frontend deps (if needed) |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. [ ] Review and approve this plan
|
||||
2. [ ] Prioritize phases (suggest starting with Phase 1 & 2)
|
||||
3. [ ] Create mockups/wireframes for key pages
|
||||
4. [ ] Implement Phase 1: Navigation & Dashboard
|
||||
5. [ ] User testing with admin users
|
||||
6. [ ] Iterate based on feedback
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Run migrations and start the app
|
||||
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]
|
||||
612
PROJECT_UNDERSTANDING.md
Normal file
612
PROJECT_UNDERSTANDING.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# Project Understanding: IRT-Powered Adaptive Question Bank System
|
||||
|
||||
> **Project Name:** IRT Bank Soal
|
||||
> **Version:** 1.0.0
|
||||
> **Last Updated:** 2026-06-15
|
||||
> **Repository:** https://git.backoffice.biz.id/dwindown/yellow-bank-soal
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#executive-summary)
|
||||
2. [Project Purpose](#project-purpose)
|
||||
3. [Tech Stack](#tech-stack)
|
||||
4. [Project Structure](#project-structure)
|
||||
5. [Core Concepts](#core-concepts)
|
||||
6. [Data Models](#data-models)
|
||||
7. [API Endpoints](#api-endpoints)
|
||||
8. [Key Services](#key-services)
|
||||
9. [Scoring Formulas](#scoring-formulas)
|
||||
10. [Configuration](#configuration)
|
||||
11. [Workflows](#workflows)
|
||||
12. [Deployment](#deployment)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This is a **FastAPI-based backend system** for managing adaptive assessment/tryout exams with sophisticated scoring capabilities. The system supports both **Classical Test Theory (CTT)** and **Item Response Theory (IRT)** scoring methods, with multi-website support for WordPress integration.
|
||||
|
||||
### Key Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **CTT Scoring** | Classical Test Theory with exact Excel formula compatibility |
|
||||
| **IRT Support** | Item Response Theory (1PL Rasch model) for adaptive testing |
|
||||
| **Multi-Site** | Single backend serving multiple WordPress sites |
|
||||
| **AI Generation** | Automatic question variant generation via OpenRouter |
|
||||
| **Excel Import/Export** | Bulk import/export questions from Excel files |
|
||||
| **Adaptive Testing** | Computer Adaptive Testing (CAT) with theta estimation |
|
||||
| **Normalization** | Static, dynamic, or hybrid score normalization |
|
||||
|
||||
---
|
||||
|
||||
## Project Purpose
|
||||
|
||||
The system replaces traditional fixed-difficulty exams with an **adaptive question bank** that:
|
||||
|
||||
1. **Measures student ability accurately** using IRT theta estimation
|
||||
2. **Provides comparable scores** across different exam sessions via normalization
|
||||
3. **Generates new questions** using AI when needed
|
||||
4. **Integrates with WordPress** LMS platforms for student access
|
||||
5. **Reduces exam fraud** by delivering different question variants to each student
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
```
|
||||
Framework: FastAPI >= 0.104.1
|
||||
Server: Uvicorn >= 0.24.0
|
||||
Database: PostgreSQL + SQLAlchemy 2.0 (async)
|
||||
ORM: SQLAlchemy >= 2.0.23
|
||||
Driver: asyncpg >= 0.29.0
|
||||
Migrations: Alembic >= 1.13.0
|
||||
Validation: Pydantic >= 2.5.0
|
||||
```
|
||||
|
||||
### Data Processing
|
||||
|
||||
```
|
||||
Excel: openpyxl >= 3.1.2, pandas >= 2.1.4
|
||||
Math/Science: numpy >= 1.26.2, scipy >= 1.11.4
|
||||
```
|
||||
|
||||
### External Integrations
|
||||
|
||||
```
|
||||
AI: OpenAI >= 1.6.1 (OpenRouter API)
|
||||
Task Queue: Celery >= 5.3.6, Redis >= 5.0.1
|
||||
Admin Panel: FastAPI-Admin >= 1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
yellow-bank-soal/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI app entry point
|
||||
│ ├── admin.py # FastAPI Admin configuration
|
||||
│ ├── admin_web.py # Admin web interface
|
||||
│ ├── database.py # Database configuration & session
|
||||
│ │
|
||||
│ ├── api/
|
||||
│ │ └── v1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── session.py # Adaptive session endpoints
|
||||
│ │
|
||||
│ ├── core/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── auth.py # Authentication & authorization
|
||||
│ │ ├── config.py # Settings from environment
|
||||
│ │ └── rate_limit.py # Rate limiting
|
||||
│ │
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── ai_generation_run.py
|
||||
│ │ ├── item.py # Question items
|
||||
│ │ ├── report_schedule.py
|
||||
│ │ ├── session.py # Student tryout sessions
|
||||
│ │ ├── tryout.py # Tryout configurations
|
||||
│ │ ├── tryout_import_snapshot.py
|
||||
│ │ ├── tryout_snapshot_question.py
|
||||
│ │ ├── tryout_stats.py # Normalization statistics
|
||||
│ │ ├── user.py
|
||||
│ │ ├── user_answer.py # Student responses
|
||||
│ │ └── website.py
|
||||
│ │
|
||||
│ ├── routers/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py # Admin-only endpoints
|
||||
│ │ ├── ai.py # AI generation endpoints
|
||||
│ │ ├── import_export.py # Excel import/export
|
||||
│ │ ├── reports.py # Report generation
|
||||
│ │ ├── sessions.py # Session management
|
||||
│ │ ├── tryouts.py # Tryout configuration
|
||||
│ │ └── wordpress.py # WordPress integration
|
||||
│ │
|
||||
│ ├── schemas/ # Pydantic request/response models
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── ai.py
|
||||
│ │ ├── report.py
|
||||
│ │ ├── session.py
|
||||
│ │ ├── tryout.py
|
||||
│ │ └── wordpress.py
|
||||
│ │
|
||||
│ └── services/
|
||||
│ ├── __init__.py
|
||||
│ ├── ai_generation.py # OpenRouter integration
|
||||
│ ├── cat_selection.py # Computer Adaptive Testing
|
||||
│ ├── config_management.py
|
||||
│ ├── ctt_scoring.py # CTT scoring engine
|
||||
│ ├── excel_import.py # Excel parsing
|
||||
│ ├── irt_calibration.py # IRT calibration
|
||||
│ ├── normalization.py
|
||||
│ ├── reporting.py
|
||||
│ ├── tryout_json_import.py
|
||||
│ └── wordpress_auth.py
|
||||
│
|
||||
├── alembic/ # Database migrations
|
||||
│ ├── env.py
|
||||
│ ├── script.py.mako
|
||||
│ └── versions/
|
||||
│
|
||||
├── tests/ # Unit & integration tests
|
||||
│ ├── test_auth_scope.py
|
||||
│ ├── test_auth_tokens.py
|
||||
│ ├── test_model_mappings.py
|
||||
│ ├── test_normalization.py
|
||||
│ ├── test_operational_hardening.py
|
||||
│ ├── test_route_wiring.py
|
||||
│ ├── test_security_regressions.py
|
||||
│ └── test_tryout_json_import.py
|
||||
│
|
||||
├── requirements.txt
|
||||
├── alembic.ini
|
||||
├── irt_1pl_mle.py # Standalone IRT MLE script
|
||||
├── PRD.md # Product Requirements Document
|
||||
├── project-brief.md # Technical specification
|
||||
└── handoff.md # Project handoff context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Tryout (Exam)
|
||||
|
||||
A **Tryout** represents a complete exam/test with configurable behavior:
|
||||
|
||||
```python
|
||||
scoring_mode: "ctt" | "irt" | "hybrid"
|
||||
selection_mode: "fixed" | "adaptive" | "hybrid"
|
||||
normalization_mode: "static" | "dynamic" | "hybrid"
|
||||
```
|
||||
|
||||
### 2. Item (Question)
|
||||
|
||||
An **Item** represents a single question with:
|
||||
|
||||
- **Content**: stem (question text), options (A/B/C/D), correct_answer
|
||||
- **CTT Parameters**: p-value (difficulty), bobot (weight)
|
||||
- **IRT Parameters**: b (difficulty), se (standard error)
|
||||
- **Metadata**: slot position, difficulty level, AI generation info
|
||||
|
||||
### 3. Session (Student Attempt)
|
||||
|
||||
A **Session** tracks a student's attempt:
|
||||
|
||||
- Links student (`wp_user_id`) to a Tryout
|
||||
- Records all answers via `UserAnswer` records
|
||||
- Stores computed scores: NM, NN, theta
|
||||
|
||||
### 4. Website (Multi-Tenant)
|
||||
|
||||
The system supports **multiple WordPress websites** from a single backend:
|
||||
|
||||
- Each website has isolated data
|
||||
- Authenticated via `X-Website-ID` header
|
||||
- WordPress JWT tokens for authentication
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### Entity Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Website ||--o{ Tryout : "hosts"
|
||||
Website ||--o{ User : "contains"
|
||||
Website ||--o{ Session : "serves"
|
||||
Website ||--o{ Item : "contains"
|
||||
|
||||
Tryout ||--o{ Item : "contains"
|
||||
Tryout ||--o{ Session : "has"
|
||||
Tryout ||--o{ TryoutStats : "tracks"
|
||||
|
||||
Session ||--o{ UserAnswer : "contains"
|
||||
Session ||--o{ User : "belongs to"
|
||||
|
||||
Item ||--o{ UserAnswer : "answered by"
|
||||
Item ||--o{ Item : "has variants"
|
||||
|
||||
AIGenerationRun ||--o{ Item : "generates"
|
||||
```
|
||||
|
||||
### Model Summary
|
||||
|
||||
| Model | Purpose | Key Fields |
|
||||
|-------|---------|------------|
|
||||
| `Website` | Multi-tenant isolation | domain, wordpress_url |
|
||||
| `User` | WordPress user mapping | wp_user_id, website_id |
|
||||
| `Tryout` | Exam configuration | scoring_mode, selection_mode, normalization_mode |
|
||||
| `Item` | Question | stem, options, ctt_p, ctt_bobot, irt_b, irt_se |
|
||||
| `Session` | Student attempt | session_id, NM, NN, theta |
|
||||
| `UserAnswer` | Single response | response, is_correct, bobot_earned |
|
||||
| `TryoutStats` | Normalization data | participant_count, rataan, sb |
|
||||
| `AIGenerationRun` | AI generation batch | model, status, items_generated |
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public API (via `/api/v1`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/tryout/{tryout_id}/config` | Get tryout configuration |
|
||||
| `PUT` | `/tryout/{tryout_id}/normalization` | Update normalization settings |
|
||||
| `GET` | `/tryout/` | List tryouts for website |
|
||||
| `GET` | `/tryout/{tryout_id}/calibration-status` | Get IRT calibration status |
|
||||
| `POST` | `/tryout/{tryout_id}/calibrate` | Trigger IRT calibration |
|
||||
| `POST` | `/session/` | Create new session |
|
||||
| `GET` | `/session/{session_id}` | Get session details |
|
||||
| `POST` | `/session/{session_id}/complete` | Submit answers, calculate scores |
|
||||
|
||||
### Admin API (requires admin role)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/ai/generate` | Generate AI questions |
|
||||
| `POST` | `/import/excel` | Import questions from Excel |
|
||||
| `GET` | `/export/excel/{tryout_id}` | Export questions to Excel |
|
||||
| `GET` | `/reports/*` | Generate various reports |
|
||||
|
||||
### Adaptive Session API (via `/api/v1/session`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/adaptive/start` | Start adaptive session |
|
||||
| `POST` | `/adaptive/respond` | Submit answer, get next item |
|
||||
| `POST` | `/adaptive/complete` | Complete adaptive session |
|
||||
|
||||
---
|
||||
|
||||
## Key Services
|
||||
|
||||
### 1. CTT Scoring Engine (`ctt_scoring.py`)
|
||||
|
||||
Implements Classical Test Theory scoring with exact Excel formulas.
|
||||
|
||||
**Key Functions:**
|
||||
- `calculate_ctt_p()` - Difficulty: p = Σ Benar / Total Peserta
|
||||
- `calculate_ctt_bobot()` - Weight: Bobot = 1 - p
|
||||
- `calculate_ctt_nm()` - Raw Score: NM = (Total_Bobot / Total_Bobot_Max) × 1000
|
||||
- `calculate_ctt_nn()` - Normalized: NN = 500 + 100 × ((NM - Rataan) / SB)
|
||||
- `categorize_difficulty()` - Categorize by p-value
|
||||
- `update_tryout_stats()` - Incrementally update normalization stats
|
||||
|
||||
### 2. IRT Calibration (`irt_calibration.py`)
|
||||
|
||||
Implements Item Response Theory (1PL Rasch model) for adaptive testing.
|
||||
|
||||
**Key Functions:**
|
||||
- `estimate_theta_mle()` - MLE theta estimation for students
|
||||
- `estimate_b()` - IRT difficulty calibration for items
|
||||
- `calibrate_item()` - Calibrate single item from response data
|
||||
- `calibrate_all()` - Batch calibrate all items in tryout
|
||||
- `calculate_fisher_information()` - Fisher information for item selection
|
||||
|
||||
**Parameters:**
|
||||
- θ (theta): Student ability [-3, +3]
|
||||
- b: Item difficulty [-3, +3]
|
||||
- Probability: P(θ) = 1 / (1 + exp(-(θ - b)))
|
||||
|
||||
### 3. AI Generation (`ai_generation.py`)
|
||||
|
||||
Generates question variants using OpenRouter API.
|
||||
|
||||
**Key Functions:**
|
||||
- `generate_question()` - Generate single question via OpenRouter
|
||||
- `generate_questions_batch()` - Generate multiple questions
|
||||
- `save_ai_question()` - Save generated question to database
|
||||
- `check_cache_reuse()` - Check for reusable similar questions
|
||||
|
||||
**Models Supported:**
|
||||
- Qwen 2.5 32B (balanced)
|
||||
- Mistral Small (low cost)
|
||||
- Llama 3.3 70B (premium)
|
||||
|
||||
### 4. Excel Import/Export (`excel_import.py`)
|
||||
|
||||
Bulk import/export questions from Excel files.
|
||||
|
||||
**Key Functions:**
|
||||
- `parse_excel_import()` - Parse Excel file to items
|
||||
- `bulk_insert_items()` - Insert parsed items to database
|
||||
- `export_questions_to_excel()` - Export tryout to Excel
|
||||
|
||||
### 5. CAT Selection (`cat_selection.py`)
|
||||
|
||||
Computer Adaptive Testing item selection algorithm.
|
||||
|
||||
**Key Functions:**
|
||||
- `select_next_item()` - Select next item based on theta estimate
|
||||
- `calculate_theta_update()` - Update theta after response
|
||||
- `check_termination()` - Check if test should end
|
||||
|
||||
---
|
||||
|
||||
## Scoring Formulas
|
||||
|
||||
### CTT (Classical Test Theory)
|
||||
|
||||
Based on exact client Excel formulas:
|
||||
|
||||
```python
|
||||
# STEP 1: Tingkat Kesukaran (p-value)
|
||||
p = Σ Benar / Total Peserta
|
||||
|
||||
# STEP 2: Bobot (Weight)
|
||||
Bobot = 1 - p
|
||||
|
||||
# STEP 3: Total Benar per Siswa
|
||||
Total_Benar = count of correct answers
|
||||
|
||||
# STEP 4: Total Bobot Earned per Siswa
|
||||
Total_Bobot_Siswa = Σ Bobot for each correct answer
|
||||
|
||||
# STEP 5: Nilai Mentah (Raw Score)
|
||||
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000
|
||||
|
||||
# STEP 6: Nilai Nasional (Normalized Score)
|
||||
NN = 500 + 100 × ((NM - Rataan) / SB)
|
||||
```
|
||||
|
||||
### IRT (Item Response Theory)
|
||||
|
||||
1PL Rasch Model:
|
||||
|
||||
```python
|
||||
# Probability of correct response
|
||||
P(θ, b) = 1 / (1 + exp(-(θ - b)))
|
||||
|
||||
# Log-likelihood for MLE
|
||||
LL = Σ [u_i × log(P) + (1-u_i) × log(1-P)]
|
||||
|
||||
# Theta estimation via MLE
|
||||
θ_mle = argmax_θ LL(θ)
|
||||
```
|
||||
|
||||
### Difficulty Categories (CTT Standard)
|
||||
|
||||
| p-value | Category | Description |
|
||||
|---------|----------|-------------|
|
||||
| p < 0.30 | Sulit | Difficult |
|
||||
| 0.30 ≤ p ≤ 0.70 | Sedang | Medium |
|
||||
| p > 0.70 | Mudah | Easy |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/irt_bank_soal
|
||||
|
||||
# FastAPI
|
||||
SECRET_KEY=your-secret-key-here
|
||||
ENVIRONMENT=development # development, staging, production
|
||||
ENABLE_ADMIN=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=your-password
|
||||
|
||||
# OpenRouter (AI)
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxx
|
||||
OPENROUTER_MODEL_QWEN=qwen/qwen2.5-32b-instruct
|
||||
OPENROUTER_MODEL_CHEAP=mistralai/mistral-small-2603
|
||||
OPENROUTER_MODEL_LLAMA=meta-llama/llama-3.3-70b-instruct
|
||||
|
||||
# Redis/Celery
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
|
||||
# CORS
|
||||
ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
|
||||
```
|
||||
|
||||
### Tryout Configuration Options
|
||||
|
||||
```python
|
||||
# Scoring Mode
|
||||
scoring_mode = "ctt" # Classical Test Theory
|
||||
scoring_mode = "irt" # Item Response Theory
|
||||
scoring_mode = "hybrid" # Both (IRT for calibration, CTT for scoring)
|
||||
|
||||
# Selection Mode
|
||||
selection_mode = "fixed" # Fixed order questions
|
||||
selection_mode = "adaptive" # Computer Adaptive Testing
|
||||
selection_mode = "hybrid" # Start fixed, switch to adaptive
|
||||
|
||||
# Normalization Mode
|
||||
normalization_mode = "static" # Use hardcoded rataan/sb
|
||||
normalization_mode = "dynamic" # Calculate from participant data
|
||||
normalization_mode = "hybrid" # Dynamic when sufficient data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflows
|
||||
|
||||
### 1. Student Taking a Tryout
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Student
|
||||
participant API as FastAPI
|
||||
participant WP as WordPress
|
||||
|
||||
S->>API: POST /session/ (start session)
|
||||
API-->>S: session_id
|
||||
|
||||
loop For each question
|
||||
S->>API: GET /session/{id}/next-item
|
||||
API-->>S: Question data
|
||||
|
||||
S->>API: POST /session/{id}/answer
|
||||
API-->>S: Next question or completion
|
||||
end
|
||||
|
||||
S->>API: POST /session/{id}/complete
|
||||
API-->>S: NM, NN scores
|
||||
```
|
||||
|
||||
### 2. Admin Importing Questions
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Upload Excel File] --> B[Parse Excel]
|
||||
B --> C{Validate Structure}
|
||||
C -->|Invalid| D[Return Error]
|
||||
C -->|Valid| E[Calculate CTT p & bobot]
|
||||
E --> F[Bulk Insert Items]
|
||||
F --> G[Commit to Database]
|
||||
G --> H[Return Import Summary]
|
||||
```
|
||||
|
||||
### 3. AI Question Generation
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Request Generation] --> B{Check Cache}
|
||||
B -->|Found similar| C[Return Cached]
|
||||
B -->|Not found| D[Call OpenRouter API]
|
||||
D --> E{Parse Response}
|
||||
E -->|Parse Error| F[Return Error]
|
||||
E -->|Success| G[Save to Database]
|
||||
G --> H[Return Generated Item]
|
||||
```
|
||||
|
||||
### 4. IRT Calibration
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Collect Responses] --> B{Enough Data?}
|
||||
B -->|No| C[Wait for more]
|
||||
B -->|Yes| D[For each Item]
|
||||
D --> E[Get Response Matrix]
|
||||
E --> F[Estimate b via MLE]
|
||||
F --> G[Calculate Standard Error]
|
||||
G --> H[Update Item]
|
||||
H --> D
|
||||
D --> I[Mark Items Calibrated]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- PostgreSQL 14+
|
||||
- Redis 6+ (for Celery)
|
||||
- Nginx (reverse proxy)
|
||||
- aaPanel with Python Manager (recommended)
|
||||
|
||||
### Running the Application
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run migrations
|
||||
alembic upgrade head
|
||||
|
||||
# Start server
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
|
||||
# Or with reload (development)
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
|
||||
- Swagger UI: `http://localhost:8000/docs`
|
||||
- ReDoc: `http://localhost:8000/redoc`
|
||||
- OpenAPI JSON: `http://localhost:8000/openapi.json`
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
|
||||
- WordPress JWT tokens for user authentication
|
||||
- `X-Website-ID` header for multi-tenant isolation
|
||||
- Admin routes protected by admin role check
|
||||
|
||||
### Production Hardening
|
||||
|
||||
1. **SECRET_KEY** must be set to a strong, unique value
|
||||
2. **ADMIN_PASSWORD** must not be the default
|
||||
3. **CORS** origins should be explicitly configured
|
||||
4. **Database** connections should use SSL in production
|
||||
5. **Rate limiting** enabled for AI generation endpoints
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Tryout** | An exam/test assessment |
|
||||
| **Item** | A single question in a tryout |
|
||||
| **Session** | A student's attempt at a tryout |
|
||||
| **CTT** | Classical Test Theory - traditional scoring |
|
||||
| **IRT** | Item Response Theory - modern adaptive scoring |
|
||||
| **NM** | Nilai Mentah - raw score [0-1000] |
|
||||
| **NN** | Nilai Nasional - normalized score [0-1000] |
|
||||
| **θ (theta)** | IRT ability estimate [-3 to +3] |
|
||||
| **b** | IRT item difficulty [-3 to +3] |
|
||||
| **p-value** | CTT proportion correct [0 to 1] |
|
||||
| **Bobot** | CTT weight (1 - p) |
|
||||
| **Rataan** | Mean (Indonesian) |
|
||||
| **SB** | Simpangan Baku - Standard Deviation |
|
||||
| **CAT** | Computer Adaptive Testing |
|
||||
| **MLE** | Maximum Likelihood Estimation |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [PRD.md](./PRD.md) - Complete Product Requirements Document
|
||||
- [project-brief.md](./project-brief.md) - Original technical specification
|
||||
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
|
||||
- [SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/)
|
||||
- [Item Response Theory](https://en.wikipedia.org/wiki/Item_response_theory)
|
||||
@@ -84,7 +84,7 @@ path_separator = os
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/irt_bank_soal
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
3117
app/admin_web.py
3117
app/admin_web.py
File diff suppressed because it is too large
Load Diff
112
app/admin_web_icons.py
Normal file
112
app/admin_web_icons.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Icon constants using inline SVG (Heroicons style).
|
||||
These replace emoji usage in the admin UI for consistent, professional icons.
|
||||
"""
|
||||
|
||||
# Navigation icons
|
||||
ICON_DASHBOARD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" /></svg>"""
|
||||
|
||||
ICON_QUESTIONS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>"""
|
||||
|
||||
ICON_IMPORT = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>"""
|
||||
|
||||
ICON_AI = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" /></svg>"""
|
||||
|
||||
ICON_EXAMS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>"""
|
||||
|
||||
ICON_REPORTS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>"""
|
||||
|
||||
ICON_SETTINGS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>"""
|
||||
|
||||
ICON_LOGOUT = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75" /></svg>"""
|
||||
|
||||
# Page icons
|
||||
ICON_TARGET = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
|
||||
|
||||
ICON_USERS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>"""
|
||||
|
||||
ICON_CALIBRATION = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15ZM21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h6" /></svg>"""
|
||||
|
||||
ICON_STUDENTS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" /></svg>"""
|
||||
|
||||
ICON_DOWNLOAD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>"""
|
||||
|
||||
ICON_UPLOAD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>"""
|
||||
|
||||
ICON_SEARCH = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>"""
|
||||
|
||||
ICON_CHECK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>"""
|
||||
|
||||
ICON_WARNING = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>"""
|
||||
|
||||
ICON_INFO = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg>"""
|
||||
|
||||
ICON_LIGHTBULB = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
|
||||
|
||||
ICON_TREND_UP = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941" /></svg>"""
|
||||
|
||||
ICON_TREND_DOWN = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6 9 12.75l4.286-4.286a11.948 11.948 0 0 1 4.306 6.43l.776 2.898m0 0 3.182-5.511m-3.182 5.51-5.511-3.181" /></svg>"""
|
||||
|
||||
# Huge icons for replacing emojis (24x24 with larger visual weight)
|
||||
ICON_HUGE_TARGET = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a8.01 8.01 0 0 0 1.5-.189m-1.5.189a8.01 8.01 0 0 1-1.5-.189m3.75 7.478a10.56 10.56 0 0 1-4.5 0m3.75 2.383a13.406 13.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
|
||||
|
||||
ICON_HUGE_USER = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>"""
|
||||
|
||||
ICON_HUGE_CHECK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>"""
|
||||
|
||||
ICON_HUGE_CLOCK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>"""
|
||||
|
||||
ICON_HUGE_ROCKET = """<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none" /><g fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linejoin="round" d="m11.801 6.49l1.486-1.486c1.673-1.673 3.862-2.367 6.18-2.48c.902-.044 1.352-.066 1.714.295c.361.362.34.812.295 1.714c-.113 2.318-.807 4.507-2.48 6.18L17.511 12.2c-1.224 1.223-1.572 1.571-1.315 2.898c.254 1.014.499 1.995-.238 2.732c-.894.895-1.71.895-2.604 0l-7.183-7.183c-.895-.894-.895-1.71 0-2.604c.737-.737 1.718-.492 2.732-.238c1.327.257 1.675-.091 2.898-1.315Z" /><path stroke-linecap="round" d="m2.5 21.5l5-5m1 5l2-2m-8-4l2-2" /><path stroke-linecap="round" stroke-linejoin="round" d="M17.125 7H17m.25 0a.25.25 0 1 1-.5 0a.25.25 0 0 1 .5 0" /></g></svg>"""
|
||||
|
||||
ICON_HUGE_CHART = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>"""
|
||||
|
||||
# Emoji to SVG icon mapping for replacement
|
||||
EMOJI_TO_ICON = {
|
||||
# Navigation & main icons
|
||||
"🏠": ICON_DASHBOARD,
|
||||
"📝": ICON_QUESTIONS,
|
||||
"📥": ICON_IMPORT,
|
||||
"🤖": ICON_AI,
|
||||
"📋": ICON_EXAMS,
|
||||
"📊": ICON_REPORTS,
|
||||
"⚙️": ICON_SETTINGS,
|
||||
"🚪": ICON_LOGOUT,
|
||||
"🎯": ICON_HUGE_TARGET,
|
||||
"👤": ICON_HUGE_USER,
|
||||
"👥": ICON_USERS,
|
||||
"⚠️": ICON_WARNING,
|
||||
"ℹ️": ICON_INFO,
|
||||
"🚀": ICON_HUGE_ROCKET,
|
||||
"✅": ICON_HUGE_CHECK,
|
||||
"❌": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>""",
|
||||
"⏳": ICON_HUGE_CLOCK,
|
||||
"📈": ICON_TREND_UP,
|
||||
"📉": ICON_TREND_DOWN,
|
||||
"💡": ICON_LIGHTBULB,
|
||||
"👋": '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="display:inline;width:28px;height:28px;margin-bottom:-4px;"><path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" /></svg>',
|
||||
"📊": ICON_REPORTS,
|
||||
"🚀": ICON_HUGE_ROCKET,
|
||||
"📈": ICON_TREND_UP,
|
||||
# Additional icons from UI
|
||||
"🌐": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" /></svg>""",
|
||||
"🔍": ICON_SEARCH,
|
||||
"📁": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" /></svg>""",
|
||||
"🔐": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>""",
|
||||
"⚡": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" /></svg>""",
|
||||
"💾": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0v3.75C20.25 20.653 16.556 22.5 12 22.5s-8.25-1.847-8.25-4.125v-3.75m-16.5 0v3.75" /></svg>""",
|
||||
"🔄": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>""",
|
||||
"🔘": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>""",
|
||||
"📍": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" /></svg>""",
|
||||
}
|
||||
|
||||
# Navigation icon mapping
|
||||
NAV_ICONS_SVG = {
|
||||
"Dashboard": ICON_DASHBOARD,
|
||||
"Questions": ICON_QUESTIONS,
|
||||
"Import Questions": ICON_IMPORT,
|
||||
"AI Generator": ICON_AI,
|
||||
"Exams": ICON_EXAMS,
|
||||
"Reports": ICON_REPORTS,
|
||||
"Settings": ICON_SETTINGS,
|
||||
"Logout": ICON_LOGOUT,
|
||||
}
|
||||
@@ -107,6 +107,7 @@ REQUIREMENTS:
|
||||
4. Only ONE correct answer
|
||||
5. Include a clear explanation of why the correct answer is correct
|
||||
6. Make the question noticeably {level_desc} - not just a minor variation
|
||||
7. Follow and preserve any HTML formatting (e.g., <p>, <br>, <b>) present in the basis question
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return ONLY a valid JSON object with this exact structure (no markdown, no code blocks):
|
||||
|
||||
37
docker-compose.dev.yml
Normal file
37
docker-compose.dev.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: irt_user
|
||||
POSTGRES_PASSWORD: dev_password
|
||||
POSTGRES_DB: irt_bank_soal
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
user: "70:70" # postgres user
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://irt_user:dev_password@postgres:5432/irt_bank_soal
|
||||
REDIS_URL: redis://redis:6379
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
volumes:
|
||||
- .:/app
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
626
docs/ALUR-APLIKASI-DAN-IRT.md
Normal file
626
docs/ALUR-APLIKASI-DAN-IRT.md
Normal file
@@ -0,0 +1,626 @@
|
||||
# Alur Aplikasi IRT-Powered Question Bank
|
||||
|
||||
Dokumen ini menjelaskan alur lengkap aplikasi dari input data hingga menghasilkan next-question berbasis IRT.
|
||||
|
||||
---
|
||||
|
||||
## 1. Arsitektur Sistem
|
||||
|
||||
### 1.1 Teknologi Stack
|
||||
|
||||
```
|
||||
Framework: FastAPI >= 0.104.1
|
||||
Database: PostgreSQL + SQLAlchemy 2.0 (async)
|
||||
AI: OpenAI (OpenRouter API)
|
||||
Admin Panel: FastAPI-Admin
|
||||
Math: numpy, scipy
|
||||
Excel: openpyxl, pandas
|
||||
```
|
||||
|
||||
### 1.2 Entity Relationship
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Website ||--o{ Tryout : "hosts"
|
||||
Website ||--o{ User : "contains"
|
||||
Website ||--o{ Session : "serves"
|
||||
Website ||--o{ Item : "contains"
|
||||
|
||||
Tryout ||--o{ Item : "contains"
|
||||
Tryout ||--o{ Session : "has"
|
||||
|
||||
Session ||--o{ UserAnswer : "contains"
|
||||
|
||||
Item ||--o{ Item : "has variants"
|
||||
Item ||--o{ UserAnswer : "answered by"
|
||||
|
||||
AIGenerationRun ||--o{ Item : "generates"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Konsep Inti
|
||||
|
||||
### 2.1 Tryout (Exam)
|
||||
|
||||
**Tryout** merepresentasikan 1 ujian lengkap dengan konfigurasi:
|
||||
|
||||
| Field | Opsi | Default | Deskripsi |
|
||||
|-------|------|---------|-----------|
|
||||
| `scoring_mode` | `ctt`, `irt`, `hybrid` | `ctt` | Metode kalkulasi score |
|
||||
| `selection_mode` | `fixed`, `adaptive`, `hybrid` | `fixed` | Strategi pemilihan soal |
|
||||
| `normalization_mode` | `static`, `dynamic`, `hybrid` | `static` | Metode normalisasi |
|
||||
|
||||
### 2.2 Item (Soal)
|
||||
|
||||
**Item** merepresentasikan 1 soal dengan parameter:
|
||||
|
||||
| Field | Deskripsi |
|
||||
|-------|-----------|
|
||||
| `stem` | Teks pertanyaan |
|
||||
| `options` | Pilihan jawaban (A/B/C/D/E) |
|
||||
| `correct_answer` | Kunci jawaban |
|
||||
| `slot` | Posisi nomor soal (1, 2, 3...) |
|
||||
| `level` | Kategori kesulitan (mudah/sedang/sulit) |
|
||||
| `parent_item_id` | ID soal original (jika ini variant) |
|
||||
| `calibrated` | Status IRT calibration |
|
||||
| `irt_b` | Item difficulty parameter |
|
||||
| `irt_se` | Standard error |
|
||||
| `ctt_p` | P-value (tingkat kesukaran CTT) |
|
||||
| `ctt_bobot` | Bobot soal = 1 - p |
|
||||
|
||||
### 2.3 Session (Percobaan Siswa)
|
||||
|
||||
**Session** melacak aktivitas siswa:
|
||||
|
||||
| Field | Deskripsi |
|
||||
|-------|-----------|
|
||||
| `session_id` | Identifier unik |
|
||||
| `wp_user_id` | ID user dari WordPress |
|
||||
| `tryout_id` | Tryout yang diambil |
|
||||
| `theta` | Kemampuan estimasi IRT |
|
||||
| `theta_se` | Standard error theta |
|
||||
| `NM` | Nilai Mentah (raw score) |
|
||||
| `NN` | Nilai Nasional (normalized) |
|
||||
| `is_completed` | Status selesai |
|
||||
|
||||
### 2.4 Website (Multi-Tenant)
|
||||
|
||||
Sistem mendukung multiple WordPress websites dari 1 backend:
|
||||
|
||||
- Isolasi data per website
|
||||
- Auth via `X-Website-ID` header
|
||||
- WordPress JWT tokens
|
||||
|
||||
---
|
||||
|
||||
## 3. Alur Input Data
|
||||
|
||||
### 3.1 Sumber Data Masuk
|
||||
|
||||
| Sumber | Format | Endpoint | Fungsi |
|
||||
|--------|--------|----------|--------|
|
||||
| Admin Import | Excel (.xlsx) | `POST /import/excel` | Bulk import dari file Excel |
|
||||
| JSON Import | JSON | `tryout_json_import.py` | Import dari JSON (LMS external) |
|
||||
| AI Generation | API Request | `POST /ai/generate` | Generate variant soal baru |
|
||||
|
||||
### 3.2 Flow Import JSON
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ADMIN: Import Tryout JSON │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Upload JSON file │
|
||||
│ └─> File berisi 1 tryout lengkap (misal: "TO 2024") │
|
||||
│ └─> Terdiri dari N soal (slot 1, 2, 3, ...) │
|
||||
│ │
|
||||
│ 2. Parse JSON │
|
||||
│ └─> Extract setiap soal → Item record │
|
||||
│ └─> Generate unique item_id │
|
||||
│ │
|
||||
│ 3. Simpan ke Database │
|
||||
│ └─> Item.calibrated = False (belum ada IRT params) │
|
||||
│ └─> Item.ctt_p = NULL (belum ada response data) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 Flow AI Generate Variants
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ADMIN: Generate AI Variants │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Pilih Item Original │
|
||||
│ └─> Ambil 1 soal dari imported tryout │
|
||||
│ │
|
||||
│ 2. Request ke OpenRouter API │
|
||||
│ └─> Kirim prompt dengan soal original │
|
||||
│ └─> Minta generate variant dengan level berbeda │
|
||||
│ │
|
||||
│ 3. Simpan Variant │
|
||||
│ └─> variant.item_id = unique_id │
|
||||
│ └─> variant.parent_item_id = original.id │
|
||||
│ └─> variant.slot = original.slot (nomor sama) │
|
||||
│ │
|
||||
│ 4. Result │
|
||||
│ └─> Slot 1: 1 original + 1 variant = 2 soal │
|
||||
│ └─> Slot 2: 1 original + 1 variant = 2 soal │
|
||||
│ └─> Total: 2N soal (N slot × 2 variant) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.4 Contoh Struktur Data Setelah Import + Generate
|
||||
|
||||
```
|
||||
Tryout: "TO-2024"
|
||||
├── Slot 1
|
||||
│ ├── Item #1 (original, calibrated=True, irt_b=0.5)
|
||||
│ └── Item #2 (variant, calibrated=True, irt_b=-0.3)
|
||||
├── Slot 2
|
||||
│ ├── Item #3 (original, calibrated=True, irt_b=0.8)
|
||||
│ └── Item #4 (variant, calibrated=True, irt_b=0.2)
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Pemrosesan Scoring
|
||||
|
||||
### 4.1 CTT (Classical Test Theory)
|
||||
|
||||
#### Step-by-Step Formula:
|
||||
|
||||
```python
|
||||
# STEP 1: Tingkat Kesukaran (p-value)
|
||||
p = Σ Benar / Total Peserta
|
||||
# Contoh: 70 siswa menjawab benar dari 100 siswa → p = 0.70
|
||||
|
||||
# STEP 2: Bobot (Weight)
|
||||
bobot = 1 - p
|
||||
# Contoh: bobot = 1 - 0.70 = 0.30
|
||||
|
||||
# STEP 3: Total Benar per Siswa
|
||||
total_benar = count(correct answers)
|
||||
|
||||
# STEP 4: Total Bobot Earned per Siswa
|
||||
total_bobot_siswa = Σ bobot for each correct answer
|
||||
# Contoh: Jawab benar 3 soal dengan bobot [0.3, 0.5, 0.2] = 1.0
|
||||
|
||||
# STEP 5: Nilai Mentah (Raw Score)
|
||||
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000
|
||||
# Contoh: NM = (1.0 / 2.5) × 1000 = 400
|
||||
|
||||
# STEP 6: Nilai Nasional (Normalized Score)
|
||||
NN = 500 + 100 × ((NM - Rataan) / SB)
|
||||
# Contoh: NN = 500 + 100 × ((400 - 450) / 80) = 437.5
|
||||
```
|
||||
|
||||
#### Kategori Kesulitan (CTT Standard):
|
||||
|
||||
| p-value | Kategori | Arti |
|
||||
|---------|----------|------|
|
||||
| p < 0.30 | Sulit | Hanya <30% siswa menjawab benar |
|
||||
| 0.30 ≤ p ≤ 0.70 | Sedang | 30-70% siswa menjawab benar |
|
||||
| p > 0.70 | Mudah | >70% siswa menjawab benar |
|
||||
|
||||
### 4.2 IRT (Item Response Theory) - 1PL Rasch Model
|
||||
|
||||
#### Formula Inti:
|
||||
|
||||
```python
|
||||
# Probability of correct response
|
||||
P(θ, b) = 1 / (1 + exp(-(θ - b)))
|
||||
|
||||
# Di mana:
|
||||
# - θ (theta) = kemampuan siswa [-3, +3]
|
||||
# - b = difficulty soal [-3, +3]
|
||||
|
||||
# Contoh:
|
||||
# - Siswa dengan θ = 0.5 menghadapi soal dengan b = 0.5
|
||||
# - P(0.5, 0.5) = 1 / (1 + exp(0)) = 0.5 (50% kemungkinan benar)
|
||||
```
|
||||
|
||||
#### Interpretasi Theta:
|
||||
|
||||
| Theta | Kemampuan | Persentase Benar (jika b=0) |
|
||||
|-------|-----------|------------------------------|
|
||||
| -3.0 | Sangat Lemah | ~5% |
|
||||
| -1.5 | Lemah | ~18% |
|
||||
| 0.0 | Rata-rata | ~50% |
|
||||
| +1.5 | Cerdas | ~82% |
|
||||
| +3.0 | Sangat Cerdas | ~95% |
|
||||
|
||||
#### Theta Estimation via MLE:
|
||||
|
||||
```python
|
||||
# Log-likelihood
|
||||
LL = Σ [u_i × log(P) + (1-u_i) × log(1-P)]
|
||||
# u_i = 1 jika benar, 0 jika salah
|
||||
|
||||
# Theta estimation = maximize LL
|
||||
θ_mle = argmax_θ LL(θ)
|
||||
```
|
||||
|
||||
### 4.3 Kombinasi Scoring Mode
|
||||
|
||||
| Konfigurasi | Arti |
|
||||
|-------------|------|
|
||||
| `scoring_mode="ctt"` | Score akhir = NM, NN |
|
||||
| `scoring_mode="irt"` | Score akhir = theta × 200 + 500 |
|
||||
| `scoring_mode="hybrid"` | CTT score + IRT theta keduanya di-track |
|
||||
|
||||
---
|
||||
|
||||
## 5. IRT Calibration
|
||||
|
||||
### 5.1 Apa Itu Calibration?
|
||||
|
||||
**IRT Calibration** adalah proses mengestimasi parameter `b` (difficulty) untuk setiap soal berdasarkan response data dari siswa.
|
||||
|
||||
### 5.2 Kapan Item Became Calibrated?
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SYARAT ITEM CALIBRATED │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Minimum Response Sample │
|
||||
│ └─> Ada cukup response data (default: 100 siswa) │
|
||||
│ │
|
||||
│ 2. IRT b Parameter │
|
||||
│ └─> Sudah diestimasi via MLE │
|
||||
│ │
|
||||
│ 3. IRT SE (Standard Error) │
|
||||
│ └─> Sudah dihitung │
|
||||
│ │
|
||||
│ 4. Item.calibrated = True │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 Flow IRT Calibration
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Collect Response Data] --> B{Have Min Sample?}
|
||||
B -->|No| C[Wait for more students]
|
||||
C --> A
|
||||
B -->|Yes| D[For each Item]
|
||||
D --> E[Build Response Matrix]
|
||||
E --> F[Estimate b via MLE]
|
||||
F --> G[Calculate Standard Error]
|
||||
G --> H[Update Item.irt_b]
|
||||
H --> I[Item.calibrated = True]
|
||||
I --> D
|
||||
D --> J[Calibration Complete]
|
||||
```
|
||||
|
||||
### 5.4 Trigger Calibration
|
||||
|
||||
Calibration bisa dipicu via:
|
||||
|
||||
1. **API Endpoint:**
|
||||
```
|
||||
POST /tryout/{tryout_id}/calibrate
|
||||
```
|
||||
|
||||
2. **Admin Panel:**
|
||||
- Buka `/admin` → Tryouts → Pilih tryout → Trigger calibration
|
||||
|
||||
3. **Background Job (jika configured):**
|
||||
- Setelah enough responses terkumpul
|
||||
|
||||
---
|
||||
|
||||
## 6. Item Selection Modes
|
||||
|
||||
### 6.1 Fixed Selection
|
||||
|
||||
**Fixed** = Soal disajikan berurutan berdasarkan slot.
|
||||
|
||||
```python
|
||||
# Flow:
|
||||
1. Siswa mulai session
|
||||
2. Ambil item dengan slot=1 (urutan terendah)
|
||||
3. Setelah dijawab, ambil slot=2
|
||||
4. Lanjutkan sampai selesai
|
||||
```
|
||||
|
||||
**Karakteristik:**
|
||||
- Predictable, urutan soal tetap
|
||||
- Tidak butuh IRT calibration
|
||||
- Semua siswa dapat soal sama di posisi sama
|
||||
|
||||
### 6.2 Adaptive Selection (CAT)
|
||||
|
||||
**Adaptive** = Soal dipilih berdasarkan kemampuan siswa saat ini (theta).
|
||||
|
||||
```python
|
||||
# Flow:
|
||||
1. Siswa mulai session (θ = 0.0, default)
|
||||
2. Pilih item dengan b ≈ θ
|
||||
3. Siswa jawab → update θ
|
||||
4. Pilih item baru dengan b ≈ θ baru
|
||||
5. Ulangi sampai terminate condition
|
||||
```
|
||||
|
||||
**Karakteristik:**
|
||||
- Personalized, setiap siswa beda soal
|
||||
- Butuh item calibrated
|
||||
- Item selection pakai Fisher Information
|
||||
|
||||
#### Fisher Information Formula:
|
||||
|
||||
```python
|
||||
# Information at current theta
|
||||
I(θ) = P(θ) × (1 - P(θ))
|
||||
|
||||
# Di mana P(θ) = 1 / (1 + exp(-(θ - b)))
|
||||
|
||||
# Item dengan MAX information dipilih
|
||||
# Maximum information = item paling informatif untuk theta saat ini
|
||||
```
|
||||
|
||||
### 6.3 Hybrid Selection
|
||||
|
||||
**Hybrid** = Gabungan fixed + adaptive.
|
||||
|
||||
```python
|
||||
# Flow:
|
||||
1. Slot 1-N: Fixed selection (sequential)
|
||||
2. Setelah slot N: Switch ke adaptive selection
|
||||
3. Theta sudah ter-update dari fixed portion
|
||||
4. Adaptive portion pakai theta untuk pilih soal
|
||||
```
|
||||
|
||||
**Parameter:**
|
||||
- `hybrid_transition_slot` = Slot dimana switch ke adaptive
|
||||
|
||||
### 6.4 Perbandingan Selection Modes
|
||||
|
||||
| Mode | Butuh Calibration | Personalisasi | Predictable |
|
||||
|------|-------------------|---------------|-------------|
|
||||
| Fixed | Tidak | Tidak | Ya |
|
||||
| Adaptive | Ya | Ya | Tidak |
|
||||
| Hybrid | Parsial | Parsial | Parsial |
|
||||
|
||||
---
|
||||
|
||||
## 7. Student Session Flow
|
||||
|
||||
### 7.1 Full Student Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Student
|
||||
participant API as FastAPI
|
||||
participant DB as Database
|
||||
|
||||
S->>API: POST /session/ (start session)
|
||||
API->>DB: Create session, θ=0.0
|
||||
DB-->>API: session_id
|
||||
API-->>S: session_id
|
||||
|
||||
loop For each question (adaptive/fixed/hybrid)
|
||||
S->>API: GET /session/{id}/next-item
|
||||
API->>DB: Query next item based on selection_mode
|
||||
DB-->>API: Item data
|
||||
API-->>S: Question
|
||||
|
||||
S->>API: POST /session/{id}/answer
|
||||
API->>API: Update θ (if adaptive)
|
||||
API->>DB: Save UserAnswer
|
||||
DB-->>API: Saved
|
||||
API-->>S: Ack + next question
|
||||
end
|
||||
|
||||
S->>API: POST /session/{id}/complete
|
||||
API->>API: Calculate NM, NN, final theta
|
||||
API->>DB: Update session
|
||||
DB-->>API: Updated
|
||||
API-->>S: Final scores
|
||||
```
|
||||
|
||||
### 7.2 Next-Item Selection Berdasarkan Mode
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SELECTION MODE = FIXED │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ SELECT * FROM items │
|
||||
│ WHERE tryout_id = ? │
|
||||
│ AND item.id NOT IN (answered_items) │
|
||||
│ ORDER BY slot ASC │
|
||||
│ LIMIT 1 │
|
||||
│ │
|
||||
│ Result: Item dengan slot terkecil yang belum dijawab │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SELECTION MODE = ADAPTIVE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ current_theta = session.theta -- e.g., 0.5 │
|
||||
│ │
|
||||
│ SELECT * FROM items │
|
||||
│ WHERE tryout_id = ? │
|
||||
│ AND calibrated = TRUE │
|
||||
│ AND item.id NOT IN (answered_items) │
|
||||
│ ORDER BY ABS(irt_b - current_theta) ASC -- terdekat │
|
||||
│ LIMIT 1 │
|
||||
│ │
|
||||
│ Result: Item dengan b ≈ θ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Konfigurasi Tryout
|
||||
|
||||
### 8.1 Semua Opsi Konfigurasi
|
||||
|
||||
```python
|
||||
# Scoring
|
||||
scoring_mode = "ctt" # ctt, irt, hybrid
|
||||
scoring_mode = "irt" #
|
||||
scoring_mode = "hybrid" #
|
||||
|
||||
# Selection
|
||||
selection_mode = "fixed" # Sequential
|
||||
selection_mode = "adaptive" # CAT based on theta
|
||||
selection_mode = "hybrid" # Fixed until transition slot
|
||||
|
||||
# Normalization
|
||||
normalization_mode = "static" # Use static_rataan, static_sb
|
||||
normalization_mode = "dynamic" # Calculate from participant data
|
||||
normalization_mode = "hybrid" # Dynamic when min_sample reached
|
||||
|
||||
# IRT Settings
|
||||
min_calibration_sample = 100 # Min responses for calibration
|
||||
theta_estimation_method = "mle" # mle, map, eap
|
||||
fallback_to_ctt_on_error = True # Fallback if IRT fails
|
||||
|
||||
# Hybrid Settings
|
||||
hybrid_transition_slot = 10 # Switch to adaptive at slot 10
|
||||
|
||||
# AI Settings
|
||||
ai_generation_enabled = True # Allow AI generated items
|
||||
```
|
||||
|
||||
### 8.2 Cara Mengubah Konfigurasi
|
||||
|
||||
#### Via Database:
|
||||
```sql
|
||||
UPDATE tryouts
|
||||
SET
|
||||
scoring_mode = 'hybrid',
|
||||
selection_mode = 'adaptive',
|
||||
normalization_mode = 'dynamic'
|
||||
WHERE tryout_id = 'your-tryout-id';
|
||||
```
|
||||
|
||||
#### Via Admin Panel:
|
||||
1. Buka `/admin`
|
||||
2. Pilih menu **Tryouts**
|
||||
3. Edit tryout yang diinginkan
|
||||
4. Ubah field-field sesuai kebutuhan
|
||||
5. Save
|
||||
|
||||
---
|
||||
|
||||
## 9. Ringkasan Alur End-to-End
|
||||
|
||||
### 9.1 Admin Flow (Sekali / Periodik)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. IMPORT TRYOUT JSON │
|
||||
│ Input: File JSON (1 tryout = 1 exam) │
|
||||
│ Output: N items dalam database │
|
||||
│ │
|
||||
│ 2. AI GENERATE VARIANTS │
|
||||
│ Input: Item original │
|
||||
│ Output: Item variant (same slot, different content) │
|
||||
│ Result: 2N items (N slot × 2 variant) │
|
||||
│ │
|
||||
│ 3. COLLECT RESPONSE DATA │
|
||||
│ Input: Student answers │
|
||||
│ Output: UserAnswer records │
|
||||
│ │
|
||||
│ 4. IRT CALIBRATION │
|
||||
│ Input: Response data (min 100 students) │
|
||||
│ Output: Item.irt_b, Item.irt_se, Item.calibrated=True │
|
||||
│ │
|
||||
│ 5. CONFIGURE TRYOUT │
|
||||
│ Input: Set selection_mode = 'adaptive' │
|
||||
│ Output: Tryout siap untuk adaptive testing │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 9.2 Student Flow (Setiap Ujian)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. START SESSION │
|
||||
│ Input: tryout_id │
|
||||
│ Output: session_id, theta=0.0 │
|
||||
│ │
|
||||
│ 2. ANSWER LOOP │
|
||||
│ For each question: │
|
||||
│ - Get next item (based on selection_mode) │
|
||||
│ - Submit answer │
|
||||
│ - If adaptive: update theta │
|
||||
│ │
|
||||
│ 3. COMPLETE SESSION │
|
||||
│ Input: All answers │
|
||||
│ Output: NM, NN, theta, completion status │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 9.3 Konsep Kunci
|
||||
|
||||
| Konsep | Penjelasan |
|
||||
|--------|------------|
|
||||
| **Tryout** | 1 exam yang di-import dari JSON |
|
||||
| **Item** | 1 soal (original atau variant) |
|
||||
| **Slot** | Posisi nomor soal (1, 2, 3...) |
|
||||
| **Variant** | Soal berbeda di slot yang sama |
|
||||
| **Calibrated** | Item sudah punya irt_b (siap untuk adaptive) |
|
||||
| **Theta** | Estimasi kemampuan siswa dalam IRT scale |
|
||||
|
||||
---
|
||||
|
||||
## 10. FAQ
|
||||
|
||||
### Q: Kenapa default scoring_mode = "ctt"?
|
||||
A: CTT lebih simpel, tidak butuh IRT calibration. Cocok untuk awal sebelum cukup data.
|
||||
|
||||
### Q: Kenapa default selection_mode = "fixed"?
|
||||
A: Fixed selection tidak butuh item calibrated. Bisa jalan langsung setelah import.
|
||||
|
||||
### Q: Bagaimana switch ke adaptive?
|
||||
A:
|
||||
1. Pastikan item sudah calibrated (`calibrated = True`)
|
||||
2. Ubah `selection_mode = 'adaptive'` di tryout
|
||||
3. Student baru akan dapat adaptive selection
|
||||
|
||||
### Q: Adaptive butuh berapa banyak data?
|
||||
A: Default `min_calibration_sample = 100`. Artinya minimal 100 siswa harus sudah menjawab sebelum calibration bisa jalan.
|
||||
|
||||
### Q: CTT dan Fixed itu sama?
|
||||
A: Tidak. Mereka orthogonal:
|
||||
- **scoring_mode** = bagaimana menghitung score akhir
|
||||
- **selection_mode** = bagaimana memilih soal berikutnya
|
||||
|
||||
### Q: Aplikasi ini membuat exam?
|
||||
A: Tidak. Aplikasi ini adalah **question bank**. Exam sudah di-import dari JSON. Aplikasi "mengembangbiakkan" soal dengan membuat variants.
|
||||
|
||||
---
|
||||
|
||||
## 11. Referensi Code
|
||||
|
||||
| File | Fungsi |
|
||||
|------|--------|
|
||||
| `app/services/ctt_scoring.py` | CTT scoring calculations |
|
||||
| `app/services/irt_calibration.py` | IRT calibration, theta estimation |
|
||||
| `app/services/cat_selection.py` | Item selection (fixed/adaptive/hybrid) |
|
||||
| `app/services/ai_generation.py` | OpenRouter AI integration |
|
||||
| `app/services/excel_import.py` | Excel import/export |
|
||||
| `app/routers/sessions.py` | Session management API |
|
||||
| `app/models/tryout.py` | Tryout model definition |
|
||||
| `app/models/item.py` | Item model definition |
|
||||
| `app/models/session.py` | Session model definition |
|
||||
|
||||
---
|
||||
|
||||
*Document version: 1.0*
|
||||
*Last updated: 2026-06-15*
|
||||
0
error.html
Normal file
0
error.html
Normal file
19
patch_css.py
Normal file
19
patch_css.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import re
|
||||
|
||||
with open("app/admin_web.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Fix activity feed CSS
|
||||
content = content.replace(
|
||||
".activity-feed li:last-child {{ border-bottom: none; }}",
|
||||
".activity-feed li:last-child {{ border-bottom: none; }}\n .activity-feed li svg, .activity-feed li svg.nav-icon, .activity-feed li svg.huge-icon {{ width: 20px; height: 20px; flex-shrink: 0; }}"
|
||||
)
|
||||
|
||||
# Fix alert CSS
|
||||
content = content.replace(
|
||||
".alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }}",
|
||||
".alert svg, .alert svg.huge-icon, .alert svg.page-icon {{ width: 24px; height: 24px; flex-shrink: 0; margin-right: 4px; margin-bottom: -4px; }}\n .alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }}"
|
||||
)
|
||||
|
||||
with open("app/admin_web.py", "w") as f:
|
||||
f.write(content)
|
||||
17
patch_icons.py
Normal file
17
patch_icons.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import re
|
||||
|
||||
with open("app/admin_web_icons.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
new_mappings = """ "📈": ICON_TREND_UP,
|
||||
"📉": ICON_TREND_DOWN,
|
||||
"💡": ICON_LIGHTBULB,
|
||||
"👋": '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="display:inline;width:28px;height:28px;margin-bottom:-4px;"><path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" /></svg>',
|
||||
"📊": ICON_REPORTS,
|
||||
"🚀": ICON_HUGE_ROCKET,
|
||||
"📈": ICON_TREND_UP,"""
|
||||
|
||||
content = content.replace(' "📈": ICON_TREND_UP,\n "📉": ICON_TREND_DOWN,', new_mappings)
|
||||
|
||||
with open("app/admin_web_icons.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -38,3 +38,6 @@ fastapi-admin>=1.0.0
|
||||
|
||||
# Utilities
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Async support
|
||||
greenlet>=2.0.0
|
||||
|
||||
66
run_local.sh
Executable file
66
run_local.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Run local development server
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting IRT Bank Soal Local Dev Server"
|
||||
echo "=========================================="
|
||||
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker not found. Please install Docker first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if docker-compose is available
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo "❌ Docker Compose not found. Please install Docker Compose first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use docker compose command (Docker Desktop includes it as a plugin)
|
||||
DOCKER_COMPOSE="docker compose"
|
||||
|
||||
# Start databases
|
||||
echo "📦 Starting PostgreSQL and Redis..."
|
||||
$DOCKER_COMPOSE -f docker-compose.dev.yml up -d postgres redis
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "⏳ Waiting for PostgreSQL..."
|
||||
for i in {1..60}; do
|
||||
if docker exec yellow-bank-soal-postgres-1 pg_isready -U irt_user -d irt_bank_soal &> /dev/null 2>&1; then
|
||||
echo "✅ PostgreSQL is ready!"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "❌ PostgreSQL failed to start"
|
||||
docker logs yellow-bank-soal-postgres-1
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Check if venv exists, create if not
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "📦 Creating Python virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# Activate venv and install dependencies
|
||||
echo "📦 Installing dependencies..."
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -q
|
||||
|
||||
# Run migrations
|
||||
echo "🔄 Running database migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
# Start the dev server
|
||||
echo ""
|
||||
echo "🎉 Starting FastAPI dev server..."
|
||||
echo " Admin UI: http://localhost:8000/admin"
|
||||
echo " API Docs: http://localhost:8000/docs"
|
||||
echo " Login: admin / admin123"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
8
test_error.py
Normal file
8
test_error.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/admin/hierarchy")
|
||||
print(response.status_code)
|
||||
print(response.text)
|
||||
9
test_fetch.py
Normal file
9
test_fetch.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import asyncio
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
client.post("/admin/login", data={"username": "admin", "password": "password"})
|
||||
response = client.get("/admin/hierarchy")
|
||||
print(response.status_code)
|
||||
print(response.text)
|
||||
Reference in New Issue
Block a user