project-init
This commit is contained in:
commit
65691a0aee
7 files changed
+897
No files matched your search
@@ -0,0 +1,83 @@
|
|||||||
|
# Student Management System
|
||||||
|
|
||||||
|
A terminal CRUD application for managing **Students**, **Guardians**, and
|
||||||
|
**Attendance** records, backed by MySQL. Built against the schema in
|
||||||
|
Tables 9.3–9.5 (STUDENT, GUARDIAN, ATTENDANCE).
|
||||||
|
|
||||||
|
## What it does on startup
|
||||||
|
|
||||||
|
1. Connects to the MySQL server at `10.0.1.40:3306`.
|
||||||
|
2. Verifies the `student_db` database exists — creates it if not.
|
||||||
|
3. Verifies the `GUARDIAN`, `STUDENT`, and `ATTENDANCE` tables exist
|
||||||
|
(in that order, since STUDENT references GUARDIAN and ATTENDANCE
|
||||||
|
references STUDENT) — creates any that are missing, with the exact
|
||||||
|
data types and constraints from the schema (primary keys, foreign
|
||||||
|
keys, `NOT NULL`, `UNIQUE` on `GPhone`, and the composite primary key
|
||||||
|
`(AttendanceDate, RollNumber)` on ATTENDANCE).
|
||||||
|
4. Launches an interactive menu for Create / Read / Update / Delete.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install the one dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set your MySQL credentials as environment variables (the host/port/
|
||||||
|
database default to the assignment's values already):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DB_USER=root
|
||||||
|
export DB_PASSWORD=your_password_here
|
||||||
|
# optional overrides — these already default correctly:
|
||||||
|
# export DB_HOST=10.0.1.40
|
||||||
|
# export DB_PORT=3306
|
||||||
|
# export DB_NAME=student_db
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|-----------------|------------------------------------------------------------------|
|
||||||
|
| `config.py` | Connection settings (reads env vars, falls back to defaults) |
|
||||||
|
| `db.py` | Connects to MySQL; verifies/creates the database and tables |
|
||||||
|
| `crud.py` | All Create/Read/Update/Delete operations for the three tables |
|
||||||
|
| `ui_helpers.py` | Input prompts/validation and table-printing for the terminal |
|
||||||
|
| `main.py` | The menu-driven application — run this |
|
||||||
|
|
||||||
|
## Using the app
|
||||||
|
|
||||||
|
The main menu has three sections:
|
||||||
|
|
||||||
|
- **Guardian management** — list, view, add, update, delete guardians.
|
||||||
|
GUID must be a 12-digit number (per the Aadhaar-number schema note);
|
||||||
|
phone, if given, must be 10 digits and is unique across guardians.
|
||||||
|
- **Student management** — list, view, add, update, delete students.
|
||||||
|
Roll number is up to 3 digits; each student must reference an
|
||||||
|
existing guardian's GUID (add the guardian first).
|
||||||
|
- **Attendance management** — mark attendance (P/A) for a student on a
|
||||||
|
date (marking the same student+date again updates it instead of
|
||||||
|
erroring), view attendance by date or by student, and delete records.
|
||||||
|
|
||||||
|
Deleting a guardian who still has students is blocked by the foreign
|
||||||
|
key (delete or reassign their students first). Deleting a student
|
||||||
|
cascades and removes their attendance records automatically.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Uses [PyMySQL](https://pymysql.readthedocs.io/), a pure-Python MySQL
|
||||||
|
client — no system libraries (like `libmysqlclient`) required.
|
||||||
|
- All input is validated against the schema's constraints (max lengths,
|
||||||
|
digit counts, date format, P/A status) before hitting the database,
|
||||||
|
and database-level errors (duplicate keys, foreign key violations)
|
||||||
|
are caught and shown as friendly messages rather than crashing.
|
||||||
|
- Tested end-to-end (connect → create DB/tables → full CRUD → constraint
|
||||||
|
error handling → cascade delete) against a local MySQL/MariaDB
|
||||||
|
instance before delivery.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Configuration for the Student Management System.
|
||||||
|
|
||||||
|
Connection settings are read from environment variables first, falling back
|
||||||
|
to the defaults below (which match the assignment's requirements). This
|
||||||
|
means you can run the app as-is, or override any value like:
|
||||||
|
|
||||||
|
DB_HOST=10.0.1.40 DB_USER=root DB_PASSWORD=secret python3 main.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_HOST = os.environ.get("DB_HOST", "10.0.1.40")
|
||||||
|
DB_PORT = int(os.environ.get("DB_PORT", "3306"))
|
||||||
|
DB_USER = os.environ.get("DB_USER", "root")
|
||||||
|
DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
|
||||||
|
DB_NAME = os.environ.get("DB_NAME", "student_db")
|
||||||
|
DB_CONNECT_TIMEOUT = int(os.environ.get("DB_CONNECT_TIMEOUT", "10"))
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
CRUD operations for GUARDIAN, STUDENT and ATTENDANCE.
|
||||||
|
|
||||||
|
Every function takes an open pymysql connection as its first argument and
|
||||||
|
manages its own commit/rollback. All functions return plain data
|
||||||
|
(dicts/lists/bool) so this module has no dependency on the UI layer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
|
||||||
|
class CrudError(Exception):
|
||||||
|
"""Raised for expected, user-facing data errors (bad FK, duplicate key, etc)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _run(conn, sql, params=None, fetch=None):
|
||||||
|
"""
|
||||||
|
Helper: execute a statement, commit, and optionally fetch results.
|
||||||
|
fetch: None (no fetch), 'one', or 'all'
|
||||||
|
Translates common MySQL errors into CrudError with a friendly message.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, params or ())
|
||||||
|
result = None
|
||||||
|
if fetch == "one":
|
||||||
|
result = cur.fetchone()
|
||||||
|
elif fetch == "all":
|
||||||
|
result = cur.fetchall()
|
||||||
|
conn.commit()
|
||||||
|
return result if fetch else cur.rowcount
|
||||||
|
except pymysql.err.IntegrityError as e:
|
||||||
|
conn.rollback()
|
||||||
|
code = e.args[0] if e.args else None
|
||||||
|
if code == 1062:
|
||||||
|
raise CrudError("A record with that primary/unique key already exists.")
|
||||||
|
if code in (1451, 1452):
|
||||||
|
raise CrudError(
|
||||||
|
"That operation violates a foreign key constraint "
|
||||||
|
"(referenced record missing, or still referenced by other rows)."
|
||||||
|
)
|
||||||
|
raise CrudError(f"Database integrity error: {e}")
|
||||||
|
except pymysql.err.DataError as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise CrudError(f"Invalid data for one or more fields: {e}")
|
||||||
|
except pymysql.err.MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise CrudError(f"Database error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- GUARDIAN --
|
||||||
|
|
||||||
|
def create_guardian(conn, guid, gname, gphone, gaddress):
|
||||||
|
_run(
|
||||||
|
conn,
|
||||||
|
"INSERT INTO GUARDIAN (GUID, GName, GPhone, GAddress) VALUES (%s, %s, %s, %s)",
|
||||||
|
(guid, gname, gphone, gaddress),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_guardian(conn, guid):
|
||||||
|
return _run(conn, "SELECT * FROM GUARDIAN WHERE GUID = %s", (guid,), fetch="one")
|
||||||
|
|
||||||
|
|
||||||
|
def list_guardians(conn):
|
||||||
|
return _run(conn, "SELECT * FROM GUARDIAN ORDER BY GUID", fetch="all")
|
||||||
|
|
||||||
|
|
||||||
|
def update_guardian(conn, guid, gname=None, gphone=None, gaddress=None):
|
||||||
|
fields, params = [], []
|
||||||
|
if gname is not None:
|
||||||
|
fields.append("GName = %s")
|
||||||
|
params.append(gname)
|
||||||
|
if gphone is not None:
|
||||||
|
fields.append("GPhone = %s")
|
||||||
|
params.append(gphone)
|
||||||
|
if gaddress is not None:
|
||||||
|
fields.append("GAddress = %s")
|
||||||
|
params.append(gaddress)
|
||||||
|
if not fields:
|
||||||
|
return 0
|
||||||
|
params.append(guid)
|
||||||
|
return _run(conn, f"UPDATE GUARDIAN SET {', '.join(fields)} WHERE GUID = %s", params)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_guardian(conn, guid):
|
||||||
|
return _run(conn, "DELETE FROM GUARDIAN WHERE GUID = %s", (guid,))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- STUDENT --
|
||||||
|
|
||||||
|
def create_student(conn, roll_number, sname, sdateofbirth, guid):
|
||||||
|
_run(
|
||||||
|
conn,
|
||||||
|
"INSERT INTO STUDENT (RollNumber, SName, SDateofBirth, GUID) VALUES (%s, %s, %s, %s)",
|
||||||
|
(roll_number, sname, sdateofbirth, guid),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_student(conn, roll_number):
|
||||||
|
return _run(conn, "SELECT * FROM STUDENT WHERE RollNumber = %s", (roll_number,), fetch="one")
|
||||||
|
|
||||||
|
|
||||||
|
def list_students(conn):
|
||||||
|
return _run(
|
||||||
|
conn,
|
||||||
|
"""
|
||||||
|
SELECT s.RollNumber, s.SName, s.SDateofBirth, s.GUID, g.GName AS GuardianName
|
||||||
|
FROM STUDENT s
|
||||||
|
LEFT JOIN GUARDIAN g ON s.GUID = g.GUID
|
||||||
|
ORDER BY s.RollNumber
|
||||||
|
""",
|
||||||
|
fetch="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_student(conn, roll_number, sname=None, sdateofbirth=None, guid=None):
|
||||||
|
fields, params = [], []
|
||||||
|
if sname is not None:
|
||||||
|
fields.append("SName = %s")
|
||||||
|
params.append(sname)
|
||||||
|
if sdateofbirth is not None:
|
||||||
|
fields.append("SDateofBirth = %s")
|
||||||
|
params.append(sdateofbirth)
|
||||||
|
if guid is not None:
|
||||||
|
fields.append("GUID = %s")
|
||||||
|
params.append(guid)
|
||||||
|
if not fields:
|
||||||
|
return 0
|
||||||
|
params.append(roll_number)
|
||||||
|
return _run(conn, f"UPDATE STUDENT SET {', '.join(fields)} WHERE RollNumber = %s", params)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_student(conn, roll_number):
|
||||||
|
return _run(conn, "DELETE FROM STUDENT WHERE RollNumber = %s", (roll_number,))
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- ATTENDANCE --
|
||||||
|
|
||||||
|
def mark_attendance(conn, attendance_date, roll_number, status):
|
||||||
|
"""Insert attendance; if a record for that date+student exists, update it instead."""
|
||||||
|
existing = _run(
|
||||||
|
conn,
|
||||||
|
"SELECT 1 FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
|
||||||
|
(attendance_date, roll_number),
|
||||||
|
fetch="one",
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
_run(
|
||||||
|
conn,
|
||||||
|
"UPDATE ATTENDANCE SET AttendanceStatus = %s "
|
||||||
|
"WHERE AttendanceDate = %s AND RollNumber = %s",
|
||||||
|
(status, attendance_date, roll_number),
|
||||||
|
)
|
||||||
|
return "updated"
|
||||||
|
else:
|
||||||
|
_run(
|
||||||
|
conn,
|
||||||
|
"INSERT INTO ATTENDANCE (AttendanceDate, RollNumber, AttendanceStatus) "
|
||||||
|
"VALUES (%s, %s, %s)",
|
||||||
|
(attendance_date, roll_number, status),
|
||||||
|
)
|
||||||
|
return "created"
|
||||||
|
|
||||||
|
|
||||||
|
def get_attendance(conn, attendance_date, roll_number):
|
||||||
|
return _run(
|
||||||
|
conn,
|
||||||
|
"SELECT * FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
|
||||||
|
(attendance_date, roll_number),
|
||||||
|
fetch="one",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_attendance_by_date(conn, attendance_date):
|
||||||
|
return _run(
|
||||||
|
conn,
|
||||||
|
"""
|
||||||
|
SELECT a.AttendanceDate, a.RollNumber, s.SName, a.AttendanceStatus
|
||||||
|
FROM ATTENDANCE a
|
||||||
|
JOIN STUDENT s ON a.RollNumber = s.RollNumber
|
||||||
|
WHERE a.AttendanceDate = %s
|
||||||
|
ORDER BY a.RollNumber
|
||||||
|
""",
|
||||||
|
(attendance_date,),
|
||||||
|
fetch="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_attendance_by_student(conn, roll_number):
|
||||||
|
return _run(
|
||||||
|
conn,
|
||||||
|
"SELECT * FROM ATTENDANCE WHERE RollNumber = %s ORDER BY AttendanceDate",
|
||||||
|
(roll_number,),
|
||||||
|
fetch="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_attendance(conn, attendance_date, roll_number):
|
||||||
|
return _run(
|
||||||
|
conn,
|
||||||
|
"DELETE FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
|
||||||
|
(attendance_date, roll_number),
|
||||||
|
)
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""
|
||||||
|
Database layer: connect to MySQL, verify/create the student_db database,
|
||||||
|
and verify/create the STUDENT, GUARDIAN and ATTENDANCE tables per the
|
||||||
|
schema (Tables 9.3, 9.4, 9.5).
|
||||||
|
|
||||||
|
Schema notes:
|
||||||
|
- GUARDIAN.GUID is the primary key (12-digit Aadhaar number, stored as CHAR(12)).
|
||||||
|
- STUDENT.GUID is a foreign key referencing GUARDIAN.GUID.
|
||||||
|
- ATTENDANCE has a composite primary key (AttendanceDate, RollNumber), and
|
||||||
|
RollNumber is also a foreign key referencing STUDENT.RollNumber.
|
||||||
|
- Because STUDENT references GUARDIAN, GUARDIAN must be created first.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
import pymysql.cursors
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
# Table creation statements, in dependency order (GUARDIAN -> STUDENT -> ATTENDANCE)
|
||||||
|
TABLE_DEFINITIONS = {
|
||||||
|
"GUARDIAN": """
|
||||||
|
CREATE TABLE GUARDIAN (
|
||||||
|
GUID CHAR(12) NOT NULL,
|
||||||
|
GName VARCHAR(20) NOT NULL,
|
||||||
|
GPhone CHAR(10) NULL,
|
||||||
|
GAddress VARCHAR(30) NOT NULL,
|
||||||
|
PRIMARY KEY (GUID),
|
||||||
|
UNIQUE (GPhone)
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
""",
|
||||||
|
"STUDENT": """
|
||||||
|
CREATE TABLE STUDENT (
|
||||||
|
RollNumber INT NOT NULL,
|
||||||
|
SName VARCHAR(20) NOT NULL,
|
||||||
|
SDateofBirth DATE NOT NULL,
|
||||||
|
GUID CHAR(12) NOT NULL,
|
||||||
|
PRIMARY KEY (RollNumber),
|
||||||
|
FOREIGN KEY (GUID) REFERENCES GUARDIAN(GUID)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
""",
|
||||||
|
"ATTENDANCE": """
|
||||||
|
CREATE TABLE ATTENDANCE (
|
||||||
|
AttendanceDate DATE NOT NULL,
|
||||||
|
RollNumber INT NOT NULL,
|
||||||
|
AttendanceStatus CHAR(1) NOT NULL,
|
||||||
|
PRIMARY KEY (AttendanceDate, RollNumber),
|
||||||
|
FOREIGN KEY (RollNumber) REFERENCES STUDENT(RollNumber)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT chk_attendance_status CHECK (AttendanceStatus IN ('P', 'A'))
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Creation order matters because of foreign keys.
|
||||||
|
TABLE_ORDER = ["GUARDIAN", "STUDENT", "ATTENDANCE"]
|
||||||
|
|
||||||
|
|
||||||
|
def connect_server(database=None):
|
||||||
|
"""Connect to the MySQL server (optionally selecting a database)."""
|
||||||
|
try:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=config.DB_HOST,
|
||||||
|
port=config.DB_PORT,
|
||||||
|
user=config.DB_USER,
|
||||||
|
password=config.DB_PASSWORD,
|
||||||
|
database=database,
|
||||||
|
connect_timeout=config.DB_CONNECT_TIMEOUT,
|
||||||
|
cursorclass=pymysql.cursors.DictCursor,
|
||||||
|
autocommit=False,
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except pymysql.err.OperationalError as e:
|
||||||
|
print(f"\n[ERROR] Could not connect to MySQL server at "
|
||||||
|
f"{config.DB_HOST}:{config.DB_PORT} as user '{config.DB_USER}'.")
|
||||||
|
print(f" Details: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_database():
|
||||||
|
"""Verify the student_db database exists; create it if it does not."""
|
||||||
|
conn = connect_server(database=None)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SHOW DATABASES LIKE %s", (config.DB_NAME,))
|
||||||
|
exists = cur.fetchone() is not None
|
||||||
|
if exists:
|
||||||
|
print(f"[OK] Database '{config.DB_NAME}' found.")
|
||||||
|
else:
|
||||||
|
print(f"[..] Database '{config.DB_NAME}' not found. Creating it...")
|
||||||
|
cur.execute(
|
||||||
|
f"CREATE DATABASE `{config.DB_NAME}` "
|
||||||
|
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
print(f"[OK] Database '{config.DB_NAME}' created.")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tables(conn):
|
||||||
|
"""Verify each required table exists; create any that are missing."""
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SHOW TABLES")
|
||||||
|
existing = {row[f"Tables_in_{config.DB_NAME}"] for row in cur.fetchall()}
|
||||||
|
|
||||||
|
for table in TABLE_ORDER:
|
||||||
|
if table in existing:
|
||||||
|
print(f"[OK] Table '{table}' found.")
|
||||||
|
else:
|
||||||
|
print(f"[..] Table '{table}' not found. Creating it...")
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(TABLE_DEFINITIONS[table])
|
||||||
|
conn.commit()
|
||||||
|
print(f"[OK] Table '{table}' created.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
"""
|
||||||
|
Full startup sequence:
|
||||||
|
1. Ensure the student_db database exists (creating it if needed).
|
||||||
|
2. Connect to student_db.
|
||||||
|
3. Ensure all required tables exist (creating any that are missing).
|
||||||
|
Returns an open connection to student_db, ready for use.
|
||||||
|
"""
|
||||||
|
print(f"Connecting to MySQL server at {config.DB_HOST}:{config.DB_PORT} ...")
|
||||||
|
ensure_database()
|
||||||
|
|
||||||
|
conn = connect_server(database=config.DB_NAME)
|
||||||
|
print(f"[OK] Connected to database '{config.DB_NAME}'.")
|
||||||
|
|
||||||
|
ensure_tables(conn)
|
||||||
|
return conn
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Student Management System
|
||||||
|
==========================
|
||||||
|
Terminal CRUD application for STUDENT / GUARDIAN / ATTENDANCE, backed by
|
||||||
|
MySQL on 10.0.1.40:3306 (student_db).
|
||||||
|
|
||||||
|
On startup this:
|
||||||
|
1. Connects to the MySQL server.
|
||||||
|
2. Verifies (or creates) the student_db database.
|
||||||
|
3. Verifies (or creates) the STUDENT, GUARDIAN and ATTENDANCE tables.
|
||||||
|
4. Launches an interactive terminal menu for CRUD operations.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
python3 main.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
import crud
|
||||||
|
import db
|
||||||
|
from ui_helpers import (
|
||||||
|
clear_hint,
|
||||||
|
pause,
|
||||||
|
print_table,
|
||||||
|
prompt_attendance_status,
|
||||||
|
prompt_date,
|
||||||
|
prompt_guid,
|
||||||
|
prompt_int,
|
||||||
|
prompt_phone,
|
||||||
|
prompt_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================ GUARDIAN UI ==
|
||||||
|
|
||||||
|
def menu_guardians(conn):
|
||||||
|
while True:
|
||||||
|
clear_hint()
|
||||||
|
print("GUARDIAN MANAGEMENT")
|
||||||
|
print(" 1. List all guardians")
|
||||||
|
print(" 2. View a guardian")
|
||||||
|
print(" 3. Add a guardian")
|
||||||
|
print(" 4. Update a guardian")
|
||||||
|
print(" 5. Delete a guardian")
|
||||||
|
print(" 0. Back to main menu")
|
||||||
|
choice = input("\nChoice: ").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if choice == "1":
|
||||||
|
print()
|
||||||
|
print_table(crud.list_guardians(conn))
|
||||||
|
pause()
|
||||||
|
elif choice == "2":
|
||||||
|
guid = prompt_guid()
|
||||||
|
row = crud.get_guardian(conn, guid)
|
||||||
|
print()
|
||||||
|
print_table([row] if row else [])
|
||||||
|
pause()
|
||||||
|
elif choice == "3":
|
||||||
|
add_guardian(conn)
|
||||||
|
elif choice == "4":
|
||||||
|
update_guardian(conn)
|
||||||
|
elif choice == "5":
|
||||||
|
delete_guardian(conn)
|
||||||
|
elif choice == "0":
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(" Invalid choice.")
|
||||||
|
except crud.CrudError as e:
|
||||||
|
print(f"\n [ERROR] {e}")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def add_guardian(conn):
|
||||||
|
print("\n-- Add Guardian --")
|
||||||
|
guid = prompt_guid()
|
||||||
|
gname = prompt_str("Guardian name", max_len=20)
|
||||||
|
gphone = prompt_phone()
|
||||||
|
gaddress = prompt_str("Guardian address", max_len=30)
|
||||||
|
crud.create_guardian(conn, guid, gname, gphone, gaddress)
|
||||||
|
print(f" [OK] Guardian {guid} added.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def update_guardian(conn):
|
||||||
|
print("\n-- Update Guardian --")
|
||||||
|
guid = prompt_guid()
|
||||||
|
existing = crud.get_guardian(conn, guid)
|
||||||
|
if not existing:
|
||||||
|
print(" No guardian found with that GUID.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
print(f" Current: {existing}")
|
||||||
|
print(" (Press Enter to leave a field unchanged)")
|
||||||
|
gname = prompt_str("New name", max_len=20, allow_blank_skip=True)
|
||||||
|
gphone_raw = input("New phone (10 digits, blank = unchanged, 'clear' = remove): ").strip()
|
||||||
|
gphone = existing["GPhone"]
|
||||||
|
if gphone_raw.lower() == "clear":
|
||||||
|
gphone = None
|
||||||
|
elif gphone_raw:
|
||||||
|
gphone = prompt_phone_validate(gphone_raw)
|
||||||
|
if gphone is None:
|
||||||
|
print(" Invalid phone — update aborted for that field.")
|
||||||
|
gphone = existing["GPhone"]
|
||||||
|
gaddress = prompt_str("New address", max_len=30, allow_blank_skip=True)
|
||||||
|
|
||||||
|
crud.update_guardian(conn, guid, gname=gname, gphone=gphone, gaddress=gaddress)
|
||||||
|
print(" [OK] Guardian updated.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_phone_validate(raw):
|
||||||
|
import re
|
||||||
|
return raw if re.match(r"^\d{10}$", raw) else None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_guardian(conn):
|
||||||
|
print("\n-- Delete Guardian --")
|
||||||
|
guid = prompt_guid()
|
||||||
|
confirm = input(f" Type 'yes' to confirm deleting guardian {guid}: ").strip().lower()
|
||||||
|
if confirm != "yes":
|
||||||
|
print(" Cancelled.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
rows = crud.delete_guardian(conn, guid)
|
||||||
|
if rows:
|
||||||
|
print(" [OK] Guardian deleted.")
|
||||||
|
else:
|
||||||
|
print(" No guardian found with that GUID.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================= STUDENT UI ==
|
||||||
|
|
||||||
|
def menu_students(conn):
|
||||||
|
while True:
|
||||||
|
clear_hint()
|
||||||
|
print("STUDENT MANAGEMENT")
|
||||||
|
print(" 1. List all students")
|
||||||
|
print(" 2. View a student")
|
||||||
|
print(" 3. Add a student")
|
||||||
|
print(" 4. Update a student")
|
||||||
|
print(" 5. Delete a student")
|
||||||
|
print(" 0. Back to main menu")
|
||||||
|
choice = input("\nChoice: ").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if choice == "1":
|
||||||
|
print()
|
||||||
|
print_table(crud.list_students(conn))
|
||||||
|
pause()
|
||||||
|
elif choice == "2":
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
row = crud.get_student(conn, roll)
|
||||||
|
print()
|
||||||
|
print_table([row] if row else [])
|
||||||
|
pause()
|
||||||
|
elif choice == "3":
|
||||||
|
add_student(conn)
|
||||||
|
elif choice == "4":
|
||||||
|
update_student(conn)
|
||||||
|
elif choice == "5":
|
||||||
|
delete_student(conn)
|
||||||
|
elif choice == "0":
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(" Invalid choice.")
|
||||||
|
except crud.CrudError as e:
|
||||||
|
print(f"\n [ERROR] {e}")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def add_student(conn):
|
||||||
|
print("\n-- Add Student --")
|
||||||
|
roll = prompt_int("Roll number (max 3 digits)", max_digits=3)
|
||||||
|
sname = prompt_str("Student name", max_len=20)
|
||||||
|
dob = prompt_date("Date of birth")
|
||||||
|
print(" (Guardian must already exist — add them first via Guardian Management if needed)")
|
||||||
|
guid = prompt_guid()
|
||||||
|
crud.create_student(conn, roll, sname, dob, guid)
|
||||||
|
print(f" [OK] Student {roll} added.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def update_student(conn):
|
||||||
|
print("\n-- Update Student --")
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
existing = crud.get_student(conn, roll)
|
||||||
|
if not existing:
|
||||||
|
print(" No student found with that roll number.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
print(f" Current: {existing}")
|
||||||
|
print(" (Press Enter to leave a field unchanged)")
|
||||||
|
sname = prompt_str("New name", max_len=20, allow_blank_skip=True)
|
||||||
|
dob = prompt_date("New date of birth", allow_blank_skip=True)
|
||||||
|
guid = prompt_guid("New guardian GUID", allow_blank_skip=True)
|
||||||
|
|
||||||
|
crud.update_student(conn, roll, sname=sname, sdateofbirth=dob, guid=guid)
|
||||||
|
print(" [OK] Student updated.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_student(conn):
|
||||||
|
print("\n-- Delete Student --")
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
confirm = input(f" Type 'yes' to confirm deleting student {roll} "
|
||||||
|
f"(also deletes their attendance records): ").strip().lower()
|
||||||
|
if confirm != "yes":
|
||||||
|
print(" Cancelled.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
rows = crud.delete_student(conn, roll)
|
||||||
|
if rows:
|
||||||
|
print(" [OK] Student deleted.")
|
||||||
|
else:
|
||||||
|
print(" No student found with that roll number.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================== ATTENDANCE UI ==
|
||||||
|
|
||||||
|
def menu_attendance(conn):
|
||||||
|
while True:
|
||||||
|
clear_hint()
|
||||||
|
print("ATTENDANCE MANAGEMENT")
|
||||||
|
print(" 1. Mark / update attendance for a student")
|
||||||
|
print(" 2. View attendance by date")
|
||||||
|
print(" 3. View attendance history for a student")
|
||||||
|
print(" 4. Delete an attendance record")
|
||||||
|
print(" 0. Back to main menu")
|
||||||
|
choice = input("\nChoice: ").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if choice == "1":
|
||||||
|
mark_attendance(conn)
|
||||||
|
elif choice == "2":
|
||||||
|
d = prompt_date("Date")
|
||||||
|
print()
|
||||||
|
print_table(crud.list_attendance_by_date(conn, d))
|
||||||
|
pause()
|
||||||
|
elif choice == "3":
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
print()
|
||||||
|
print_table(crud.list_attendance_by_student(conn, roll))
|
||||||
|
pause()
|
||||||
|
elif choice == "4":
|
||||||
|
delete_attendance(conn)
|
||||||
|
elif choice == "0":
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(" Invalid choice.")
|
||||||
|
except crud.CrudError as e:
|
||||||
|
print(f"\n [ERROR] {e}")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def mark_attendance(conn):
|
||||||
|
print("\n-- Mark Attendance --")
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
if not crud.get_student(conn, roll):
|
||||||
|
print(" No student found with that roll number.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
d = prompt_date("Date")
|
||||||
|
status = prompt_attendance_status()
|
||||||
|
result = crud.mark_attendance(conn, d, roll, status)
|
||||||
|
print(f" [OK] Attendance {result} for student {roll} on {d}: {status}")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_attendance(conn):
|
||||||
|
print("\n-- Delete Attendance Record --")
|
||||||
|
roll = prompt_int("Roll number", max_digits=3)
|
||||||
|
d = prompt_date("Date")
|
||||||
|
confirm = input(" Type 'yes' to confirm: ").strip().lower()
|
||||||
|
if confirm != "yes":
|
||||||
|
print(" Cancelled.")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
rows = crud.delete_attendance(conn, d, roll)
|
||||||
|
if rows:
|
||||||
|
print(" [OK] Attendance record deleted.")
|
||||||
|
else:
|
||||||
|
print(" No matching attendance record found.")
|
||||||
|
pause()
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ MAIN ====
|
||||||
|
|
||||||
|
def main_menu(conn):
|
||||||
|
while True:
|
||||||
|
clear_hint()
|
||||||
|
print("STUDENT MANAGEMENT SYSTEM")
|
||||||
|
print(" 1. Guardian management")
|
||||||
|
print(" 2. Student management")
|
||||||
|
print(" 3. Attendance management")
|
||||||
|
print(" 0. Exit")
|
||||||
|
choice = input("\nChoice: ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
menu_guardians(conn)
|
||||||
|
elif choice == "2":
|
||||||
|
menu_students(conn)
|
||||||
|
elif choice == "3":
|
||||||
|
menu_attendance(conn)
|
||||||
|
elif choice == "0":
|
||||||
|
print("\nGoodbye!")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(" Invalid choice.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print(" STUDENT MANAGEMENT SYSTEM - Startup")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
conn = db.get_connection()
|
||||||
|
|
||||||
|
print("\nAll checks passed. Starting application...\n")
|
||||||
|
try:
|
||||||
|
main_menu(conn)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nInterrupted. Exiting.")
|
||||||
|
except pymysql.err.MySQLError as e:
|
||||||
|
print(f"\n[ERROR] Lost database connection: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PyMySQL>=1.1.0
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
"""Small input-prompting and formatting helpers shared by the menu screens."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
GUID_RE = re.compile(r"^\d{12}$")
|
||||||
|
PHONE_RE = re.compile(r"^\d{10}$")
|
||||||
|
|
||||||
|
|
||||||
|
def pause():
|
||||||
|
input("\nPress Enter to continue...")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_hint():
|
||||||
|
print("\n" + "-" * 60 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_str(label, max_len=None, required=True, allow_blank_skip=False):
|
||||||
|
"""Prompt for a string. allow_blank_skip=True returns None on empty input (for 'leave unchanged')."""
|
||||||
|
while True:
|
||||||
|
suffix = f" (max {max_len} chars)" if max_len else ""
|
||||||
|
val = input(f"{label}{suffix}: ").strip()
|
||||||
|
if not val:
|
||||||
|
if allow_blank_skip:
|
||||||
|
return None
|
||||||
|
if required:
|
||||||
|
print(" This field is required.")
|
||||||
|
continue
|
||||||
|
return ""
|
||||||
|
if max_len and len(val) > max_len:
|
||||||
|
print(f" Too long — max {max_len} characters.")
|
||||||
|
continue
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_int(label, max_digits=None, allow_blank_skip=False):
|
||||||
|
while True:
|
||||||
|
val = input(f"{label}: ").strip()
|
||||||
|
if not val:
|
||||||
|
if allow_blank_skip:
|
||||||
|
return None
|
||||||
|
print(" This field is required.")
|
||||||
|
continue
|
||||||
|
if not val.isdigit():
|
||||||
|
print(" Please enter a whole number.")
|
||||||
|
continue
|
||||||
|
if max_digits and len(val) > max_digits:
|
||||||
|
print(f" Too many digits — max {max_digits}.")
|
||||||
|
continue
|
||||||
|
return int(val)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_guid(label="Guardian ID / GUID (12-digit Aadhaar number)", allow_blank_skip=False):
|
||||||
|
while True:
|
||||||
|
val = input(f"{label}: ").strip()
|
||||||
|
if not val:
|
||||||
|
if allow_blank_skip:
|
||||||
|
return None
|
||||||
|
print(" This field is required.")
|
||||||
|
continue
|
||||||
|
if not GUID_RE.match(val):
|
||||||
|
print(" GUID must be exactly 12 digits.")
|
||||||
|
continue
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_phone(label="Guardian phone (10 digits, optional)", allow_blank_skip=True):
|
||||||
|
while True:
|
||||||
|
val = input(f"{label}: ").strip()
|
||||||
|
if not val:
|
||||||
|
return None # NULL is allowed for GPhone
|
||||||
|
if not PHONE_RE.match(val):
|
||||||
|
print(" Phone must be exactly 10 digits (or leave blank).")
|
||||||
|
continue
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_date(label, allow_blank_skip=False):
|
||||||
|
while True:
|
||||||
|
val = input(f"{label} (YYYY-MM-DD): ").strip()
|
||||||
|
if not val:
|
||||||
|
if allow_blank_skip:
|
||||||
|
return None
|
||||||
|
print(" This field is required.")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(val, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
print(" Invalid date — use format YYYY-MM-DD, e.g. 2015-08-21.")
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_attendance_status(label="Status (P=present, A=absent)"):
|
||||||
|
while True:
|
||||||
|
val = input(f"{label}: ").strip().upper()
|
||||||
|
if val in ("P", "A"):
|
||||||
|
return val
|
||||||
|
print(" Please enter 'P' or 'A'.")
|
||||||
|
|
||||||
|
|
||||||
|
def print_table(rows, columns=None):
|
||||||
|
"""Pretty-print a list of dict rows as a simple aligned table."""
|
||||||
|
if not rows:
|
||||||
|
print(" (no records found)")
|
||||||
|
return
|
||||||
|
|
||||||
|
if columns is None:
|
||||||
|
columns = list(rows[0].keys())
|
||||||
|
|
||||||
|
widths = {}
|
||||||
|
for col in columns:
|
||||||
|
widths[col] = max(len(str(col)), *(len(str(r.get(col, ""))) for r in rows))
|
||||||
|
|
||||||
|
header = " " + " | ".join(str(col).ljust(widths[col]) for col in columns)
|
||||||
|
print(header)
|
||||||
|
print(" " + "-+-".join("-" * widths[col] for col in columns))
|
||||||
|
for r in rows:
|
||||||
|
print(" " + " | ".join(str(r.get(col, "")).ljust(widths[col]) for col in columns))
|
||||||
|
print(f"\n ({len(rows)} record{'s' if len(rows) != 1 else ''})")
|
||||||
Reference in new issue
Block a user