Excel Import/Export

Detail modul excel_import.py - format Excel klien, validasi struktur, bulk insert, ekspor.

Modul: Excel Import/Export

Source: backend/app/services/excel_import.py Lihat juga: Operasional → Instalasi

Tanggung Jawab

Impor dan ekspor soal dari/ke format Excel klien. Mendukung:

  • Validasi struktur file Excel
  • Parse metadata (KUNCI, TK, BOBOT) + data soal
  • Bulk insert ke tabel items
  • Ekspor tryout ke format Excel yang sama (round-trip)

Format Excel Klien

File .xlsx dengan struktur tetap. Sheet wajib: CONTOH.

Layout Baris & Kolom

BarisIsiCara Baca
Row 2KUNCI (answer key)data_only=False
Row 4TK (p-value terhitung)data_only=True
Row 5BOBOT (weight)data_only=True
Row 6+Data soal per barisnormal

Penting: Row 4 dan 5 berisi hasil formula Excel. Untuk membaca nilai terhitung (bukan formula string), harus pakai openpyxl.load_workbook(file, data_only=True).

Kolom Data Soal (Row 6+)

KolomFieldContoh
1slot1
2levelsedang
3soal_text (stem)"Berapa hasil 2+2?"
4-7options A, B, C, D"4", "5", "6", "7"
8correct_answer"A"
9+explanation (opsional)"Karena 2+2=4"

Minimum 8 kolom wajib.

Fungsi Publik

validate_excel_structure

python
def validate_excel_structure(file_path: str) -> Dict[str, Any]:
    """
    Validasi file Excel sebelum di-parse.

    Returns:
        {"valid": bool, "errors": List[str]}
    """

Cek Validasi

CheckPesan Error jika Gagal
File exists"File not found: {path}"
Extension .xlsx"File must be .xlsx format"
Sheet CONTOH ada'Sheet "CONTOH" not found'
Min 6 baris"Excel file must have at least 6 rows"
Min 8 kolom"Excel file must have at least 8 columns"
Row 2 (KUNCI) ada value"Row 2 (KUNCI) must contain answer key values"
Row 4 (TK) numeric"Row 4 (TK) must contain numeric p-values"
Row 5 (BOBOT) numeric"Row 5 (BOBOT) must contain numeric weight values"

parse_excel_import

python
def parse_excel_import(
    file_path: str,
    website_id: int,
    tryout_id: str
) -> ParsedExcelImport:
    """
    Parse file Excel jadi struktur internal.

    Returns:
        ParsedExcelImport berisi:
        - items: List[ItemDict]
        - metadata: {kunci, tk_values, bobot_values}
        - stats: {total_rows, valid_rows, skipped_rows}
    """

Langkah:

  1. Validate structure (validate_excel_structure)
  2. Baca KUNCI dari Row 2 (data_only=False)
  3. Baca TK & BOBOT dari Row 4-5 (data_only=True)
  4. Iterasi Row 6+ untuk extract data soal
  5. Map ke struktur internal ItemDict
  6. Compute stats (valid/skipped)

bulk_insert_items

python
async def bulk_insert_items(
    db: AsyncSession,
    items: List[ItemDict],
    website_id: int,
    tryout_id: str
) -> BulkInsertResult:
    """
    Bulk insert parsed items ke tabel `items`.

    Returns:
        BulkInsertResult:
        - inserted_count: int
        - failed_count: int
        - errors: List[str] (per row yang gagal)
    """

Menggunakan sqlalchemy bulk operations (session.add_all()) untuk efisiensi.

export_questions_to_excel

python
async def export_questions_to_excel(
    db: AsyncSession,
    website_id: int,
    tryout_id: str,
    output_path: Optional[str] = None
) -> str:
    """
    Export semua Item dari tryout ke format Excel klien.

    Returns:
        Path file .xlsx yang di-generate
    """

Round-trip fidelity: hasil ekspor harus bisa di-impor ulang dengan struktur identik.

Workflow Impor End-to-End

flowchart TD
    A["Admin upload .xlsx"] --> B["validate_excel_structure"]
    B -->|"invalid"| C["Return error list"]
    B -->|"valid"| D["parse_excel_import"]
    D --> E["Baca KUNCI row 2"]
    D --> F["Baca TK row 4 data_only=True"]
    D --> G["Baca BOBOT row 5 data_only=True"]
    E --> H["Iterasi row 6+ extract soal"]
    F --> H
    G --> H
    H --> I["Map ke ItemDict + compute p/bobot"]
    I --> J["bulk_insert_items ke DB"]
    J --> K["Return summary: inserted/failed"]

Handling Formula Excel

Excel klien punya formula di Row 4 (TK) dan Row 5 (BOBOT).openpyxl punya 2 mode baca:

python
# Mode 1: baca formula sebagai string
wb = openpyxl.load_workbook(file, data_only=False)
ws["A4"].value  # -> "=SUM(...)/COUNT(...)"

# Mode 2: baca hasil terhitung (cached value)
wb = openpyxl.load_workbook(file, data_only=True)
ws["A4"].value  # -> 0.7234 (numeric)

Caveat: data_only=True hanya bekerja kalau file terakhir disimpan oleh Excel/Calc (yang meng-cache nilai formula). File yang dibuat via script tanpa pembukaan Excel akan punya value = None untuk cell berformula.

Error Handling

Strategi

  • Fail-fast di validate_excel_structure — hentikan kalau struktur dasar salah
  • Skip & log di parse_excel_import — skip row individual yang invalid, lanjutkan row lain
  • Atomic insert di bulk_insert_items — gunakan transaction, rollback semua kalau ada error fatal

Error yang Umum

ErrorPenyebabSolusi
Sheet "CONTOH" not foundUser pakai template berbedaPakai template standard
Row 4 (TK) must contain numeric p-valuesFile belum pernah dibuka di ExcelBuka & save ulang di Excel
Row 2 (KUNJI) must contain answer key valuesKUNCI kosongIsi answer key sebelum upload
Excel file must have at least 8 columnsKolom option kurangPastikan 4 options A-D
Parse error di Row NData tidak sesuai formatLihat detail di error message

Endpoint API

MethodEndpointBody/Response
POST/api/v1/import/excelmultipart/form-data dengan file
GET/api/v1/export/excel/{tryout_id}Response: file .xlsx (download)

Detail: API → Admin.

Dependency

  • openpyxl (baca/tulis .xlsx)
  • pandas (manipulasi data untuk laporan)
  • sqlalchemy async session
  • Model: Item, Tryout

Bacaan Lanjutan

Last updated Jul 25, 2026