196 lines
5.1 KiB
Python
196 lines
5.1 KiB
Python
import sqlite3
|
|
from contextlib import contextmanager
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Generator, Optional
|
|
|
|
from config import AppConfig
|
|
|
|
|
|
class DatabaseError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class Database:
|
|
def __init__(self, config: AppConfig) -> None:
|
|
self.config = config
|
|
self.driver = config.db_driver
|
|
|
|
if self.driver not in {"mysql", "sqlite"}:
|
|
raise DatabaseError("DB_DRIVER harus 'mysql' atau 'sqlite'.")
|
|
|
|
self._mysql_connector = None
|
|
if self.driver == "mysql":
|
|
try:
|
|
import mysql.connector # type: ignore
|
|
|
|
self._mysql_connector = mysql.connector
|
|
except ImportError as exc:
|
|
raise DatabaseError(
|
|
"mysql-connector-python belum terpasang. Install dependency terlebih dulu."
|
|
) from exc
|
|
|
|
@contextmanager
|
|
def connection(self) -> Generator[Any, None, None]:
|
|
if self.driver == "sqlite":
|
|
db_path = str(self.config.project_root / "app_presensiku.sqlite3")
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return
|
|
|
|
conn = self._mysql_connector.connect(
|
|
host=self.config.db_host,
|
|
port=self.config.db_port,
|
|
database=self.config.db_name,
|
|
user=self.config.db_user,
|
|
password=self.config.db_password,
|
|
)
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
def fetch_all(self, query: str, params: tuple = ()) -> list:
|
|
"""Execute a query and fetch all results.
|
|
|
|
Args:
|
|
query: SQL query string
|
|
params: Query parameters (for parameterized queries)
|
|
|
|
Returns: List of tuples (for sqlite) or list of dicts (for mysql)
|
|
"""
|
|
if self.driver == "sqlite":
|
|
with self.connection() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
return rows or []
|
|
|
|
with self.connection() as conn:
|
|
cur = conn.cursor(dictionary=True)
|
|
cur.execute(query, params)
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return rows or []
|
|
|
|
def find_student_by_name(self, recognized_name: str) -> Optional[Dict[str, Any]]:
|
|
"""Find student by name from Laravel students table.
|
|
|
|
Returns: {"id": int, "name": str, "id_class": int} or None
|
|
"""
|
|
student_table = self.config.student_table
|
|
id_col = self.config.student_id_column
|
|
name_col = self.config.student_name_column
|
|
class_col = self.config.student_class_column
|
|
|
|
if self.driver == "sqlite":
|
|
with self.connection() as conn:
|
|
row = conn.execute(
|
|
f"SELECT {id_col}, {name_col}, {class_col} FROM {student_table} WHERE lower({name_col}) = lower(?) LIMIT 1",
|
|
(recognized_name,),
|
|
).fetchone()
|
|
if row is None:
|
|
return None
|
|
return {
|
|
"id": row[id_col],
|
|
"name": row[name_col],
|
|
"id_class": row[class_col]
|
|
}
|
|
|
|
with self.connection() as conn:
|
|
cur = conn.cursor(dictionary=True)
|
|
cur.execute(
|
|
f"SELECT {id_col} AS id, {name_col} AS name, {class_col} AS id_class FROM {student_table} "
|
|
f"WHERE LOWER({name_col}) = LOWER(%s) LIMIT 1",
|
|
(recognized_name,),
|
|
)
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
return row
|
|
|
|
def has_attendance_today(self, student_id: int, id_class: int) -> bool:
|
|
"""Check if student already has attendance recorded today for this class."""
|
|
attendance_table = self.config.attendance_table
|
|
now = datetime.now()
|
|
day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
if self.driver == "sqlite":
|
|
with self.connection() as conn:
|
|
row = conn.execute(
|
|
f"SELECT id FROM {attendance_table} WHERE id_student = ? "
|
|
f"AND id_class = ? AND created_at >= ? LIMIT 1",
|
|
(student_id, id_class, day_start.isoformat()),
|
|
).fetchone()
|
|
return row is not None
|
|
|
|
with self.connection() as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
f"SELECT id FROM {attendance_table} WHERE id_student = %s "
|
|
f"AND id_class = %s AND DATE(created_at) = DATE(%s) LIMIT 1",
|
|
(student_id, id_class, now),
|
|
)
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
return row is not None
|
|
|
|
def insert_attendance(
|
|
self,
|
|
id_student: int,
|
|
id_class: int,
|
|
picture_filename: str,
|
|
status: str = "tepat waktu",
|
|
) -> int:
|
|
"""Insert attendance record into attendance_history_dailys table.
|
|
|
|
Args:
|
|
- id_student: Student ID from students table
|
|
- id_class: Class ID from clases table
|
|
- picture_filename: Name/path of the picture file
|
|
- status: 'tepat waktu' or 'terlambat'
|
|
|
|
Returns: Attendance ID
|
|
"""
|
|
attendance_table = self.config.attendance_table
|
|
now = datetime.now()
|
|
|
|
if self.driver == "sqlite":
|
|
with self.connection() as conn:
|
|
cur = conn.execute(
|
|
f"INSERT INTO {attendance_table} "
|
|
"(id_student, id_class, picture, status, created_at, updated_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
(
|
|
id_student,
|
|
id_class,
|
|
picture_filename,
|
|
status,
|
|
now.isoformat(),
|
|
now.isoformat(),
|
|
),
|
|
)
|
|
return int(cur.lastrowid)
|
|
|
|
with self.connection() as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
f"INSERT INTO {attendance_table} "
|
|
"(id_student, id_class, picture, status, created_at, updated_at) "
|
|
"VALUES (%s, %s, %s, %s, %s, %s)",
|
|
(
|
|
id_student,
|
|
id_class,
|
|
picture_filename,
|
|
status,
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
attendance_id = int(cur.lastrowid)
|
|
cur.close()
|
|
return attendance_id
|
|
|