Student data import implementation: added tables, migration, import script, tests, and documentation.
This commit is contained in:
parent
9662f98169
commit
2959cd5299
19 changed files with 3186 additions and 79 deletions
423
app.py
423
app.py
|
|
@ -124,6 +124,47 @@ def init_db():
|
|||
)
|
||||
''')
|
||||
|
||||
# Create students table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_number INTEGER,
|
||||
cial_code TEXT UNIQUE NOT NULL,
|
||||
nif_nie_passport TEXT UNIQUE,
|
||||
registration_number INTEGER,
|
||||
file_number TEXT,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
birth_date TEXT,
|
||||
gender TEXT CHECK(gender IN ('M', 'F', 'O')),
|
||||
study_group TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT unique_identification UNIQUE (cial_code, nif_nie_passport)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create indexes for students
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_cial ON students(cial_code)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_nif ON students(nif_nie_passport)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_name ON students(last_name, first_name)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_birth_date ON students(birth_date)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_gender ON students(gender)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_students_group ON students(study_group)
|
||||
''')
|
||||
|
||||
# Create non_loanable_devices table for devices that cannot be loaned
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
||||
|
|
@ -136,10 +177,19 @@ def init_db():
|
|||
status TEXT DEFAULT 'available',
|
||||
notes TEXT,
|
||||
purchase_date TEXT,
|
||||
purchase_cost REAL
|
||||
purchase_cost REAL,
|
||||
assigned_to_student INTEGER,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (assigned_to_student) REFERENCES students(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create index for non_loanable_devices student relationship
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_non_loanable_student ON non_loanable_devices(assigned_to_student)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
|
||||
@app.route('/')
|
||||
|
|
@ -222,23 +272,24 @@ def loan_tablet():
|
|||
if request.method == 'POST':
|
||||
tablet_id = request.form['tablet_id']
|
||||
user_id = request.form['user_id']
|
||||
|
||||
student_id = request.form.get('student_id') # Optional - can be loaned to student directly
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
# Update tablet status
|
||||
cursor.execute("UPDATE tablets SET status = 'loaned' WHERE id = ?", (tablet_id,))
|
||||
|
||||
|
||||
# Create loan record
|
||||
loan_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
cursor.execute('''
|
||||
INSERT INTO loans (tablet_id, user_id, loan_date, status)
|
||||
VALUES (?, ?, ?, 'active')
|
||||
''', (tablet_id, user_id, loan_date))
|
||||
|
||||
INSERT INTO loans (tablet_id, user_id, student_id, loan_date, status)
|
||||
VALUES (?, ?, ?, ?, 'active')
|
||||
''', (tablet_id, user_id, student_id, loan_date))
|
||||
|
||||
conn.commit()
|
||||
flash('Tablet loaned successfully!', 'success')
|
||||
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Get data for form
|
||||
|
|
@ -248,10 +299,362 @@ def loan_tablet():
|
|||
available_tablets = cursor.fetchall()
|
||||
cursor.execute("SELECT * FROM users")
|
||||
users = cursor.fetchall()
|
||||
cursor.execute("SELECT * FROM students ORDER BY last_name, first_name")
|
||||
students = cursor.fetchall()
|
||||
|
||||
return render_template('loan_tablet.html',
|
||||
available_tablets=available_tablets,
|
||||
users=users)
|
||||
users=users, students=students)
|
||||
|
||||
@app.route('/students')
|
||||
def list_students():
|
||||
"""List all students with pagination and search"""
|
||||
from flask import request
|
||||
|
||||
# Get query parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search', '').strip()
|
||||
gender_filter = request.args.get('gender', '')
|
||||
group_filter = request.args.get('group', '')
|
||||
|
||||
# Pagination settings
|
||||
per_page = 50
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build query
|
||||
query = "SELECT * FROM students"
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
# Add search filter
|
||||
if search:
|
||||
conditions.append("(last_name LIKE ? OR first_name LIKE ? OR full_name LIKE ? OR cial_code LIKE ? OR nif_nie_passport LIKE ?)")
|
||||
search_param = f"%{search}%"
|
||||
params.extend([search_param] * 5)
|
||||
|
||||
# Add gender filter
|
||||
if gender_filter and gender_filter != 'all':
|
||||
conditions.append("gender = ?")
|
||||
params.append(gender_filter)
|
||||
|
||||
# Add group filter
|
||||
if group_filter and group_filter != 'all':
|
||||
conditions.append("study_group LIKE ?")
|
||||
params.append(f"%{group_filter}%")
|
||||
|
||||
# Combine conditions
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
query += " ORDER BY last_name, first_name COLLATE NOCASE"
|
||||
|
||||
# Get total count for pagination
|
||||
count_query = f"SELECT COUNT(*) FROM students"
|
||||
if conditions:
|
||||
count_query += " WHERE " + " AND ".join(conditions)
|
||||
cursor.execute(count_query, params)
|
||||
total_students = cursor.fetchone()[0]
|
||||
total_pages = (total_students + per_page - 1) // per_page
|
||||
|
||||
# Add pagination to query
|
||||
query += f" LIMIT {per_page} OFFSET {offset}"
|
||||
|
||||
# Execute main query
|
||||
cursor.execute(query, params)
|
||||
students = cursor.fetchall()
|
||||
|
||||
# Get distinct values for filters
|
||||
cursor.execute("SELECT DISTINCT gender FROM students WHERE gender IS NOT NULL ORDER BY gender")
|
||||
genders = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
cursor.execute("SELECT DISTINCT study_group FROM students WHERE study_group IS NOT NULL ORDER BY study_group")
|
||||
groups = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
return render_template('students.html',
|
||||
students=students,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
total_students=total_students,
|
||||
search=search,
|
||||
gender=gender_filter,
|
||||
group=group_filter,
|
||||
genders=genders,
|
||||
groups=groups,
|
||||
per_page=per_page)
|
||||
|
||||
|
||||
@app.route('/add_student', methods=['GET', 'POST'])
|
||||
def add_student():
|
||||
"""Add a new student"""
|
||||
if request.method == 'POST':
|
||||
order_number = request.form.get('order_number')
|
||||
cial_code = request.form.get('cial_code', '').strip()
|
||||
nif_nie_passport = request.form.get('nif_nie_passport', '').strip()
|
||||
registration_number = request.form.get('registration_number')
|
||||
file_number = request.form.get('file_number', '').strip()
|
||||
first_name = request.form.get('first_name', '').strip()
|
||||
last_name = request.form.get('last_name', '').strip()
|
||||
full_name = f"{last_name}, {first_name}" if last_name and first_name else request.form.get('full_name', '').strip()
|
||||
birth_date = request.form.get('birth_date', '').strip()
|
||||
gender = request.form.get('gender', '').strip()
|
||||
study_group = request.form.get('study_group', '').strip()
|
||||
|
||||
# Validate required fields
|
||||
if not cial_code:
|
||||
flash('Error: CIAL code is required!', 'error')
|
||||
return render_template('add_student.html')
|
||||
|
||||
if not first_name:
|
||||
flash('Error: First name is required!', 'error')
|
||||
return render_template('add_student.html')
|
||||
|
||||
if not last_name:
|
||||
flash('Error: Last name is required!', 'error')
|
||||
return render_template('add_student.html')
|
||||
|
||||
# Validate gender
|
||||
if gender and gender.upper() not in ['M', 'F', 'O']:
|
||||
flash('Error: Invalid gender! Must be M, F, or O.', 'error')
|
||||
return render_template('add_student.html')
|
||||
|
||||
# Validate date format
|
||||
if birth_date:
|
||||
try:
|
||||
datetime.strptime(birth_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
flash('Error: Invalid date format! Use YYYY-MM-DD.', 'error')
|
||||
return render_template('add_student.html')
|
||||
|
||||
try:
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO students
|
||||
(order_number, cial_code, nif_nie_passport, registration_number, file_number,
|
||||
first_name, last_name, full_name, birth_date, gender, study_group, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
''', (order_number, cial_code, nif_nie_passport or None, registration_number,
|
||||
file_number or None, first_name, last_name, full_name,
|
||||
birth_date or None, gender or None, study_group or None))
|
||||
conn.commit()
|
||||
flash('Student added successfully!', 'success')
|
||||
return redirect(url_for('list_students'))
|
||||
except sqlite3.IntegrityError as e:
|
||||
if 'UNIQUE constraint failed: students.cial_code' in str(e):
|
||||
flash('Error: CIAL code already exists!', 'error')
|
||||
elif 'UNIQUE constraint failed: students.nif_nie_passport' in str(e):
|
||||
flash('Error: NIF/NIE/Passport already exists!', 'error')
|
||||
else:
|
||||
flash(f'Error: {str(e)}', 'error')
|
||||
|
||||
return render_template('add_student.html')
|
||||
|
||||
|
||||
@app.route('/student/<int:student_id>')
|
||||
def student_detail(student_id):
|
||||
"""Show details for a specific student"""
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get student info
|
||||
cursor.execute("SELECT * FROM students WHERE id = ?", (student_id,))
|
||||
student = cursor.fetchone()
|
||||
|
||||
if not student:
|
||||
flash('Error: Student not found!', 'error')
|
||||
return redirect(url_for('list_students'))
|
||||
|
||||
# Get tablets assigned to this student
|
||||
cursor.execute('''
|
||||
SELECT t.* FROM tablets t
|
||||
WHERE t.assigned_to_student = ?
|
||||
''', (student_id,))
|
||||
assigned_tablets = cursor.fetchall()
|
||||
|
||||
# Get non-loanable devices assigned to this student
|
||||
cursor.execute('''
|
||||
SELECT * FROM non_loanable_devices
|
||||
WHERE assigned_to_student = ?
|
||||
''', (student_id,))
|
||||
assigned_devices = cursor.fetchall()
|
||||
|
||||
# Get loan history for this student
|
||||
cursor.execute('''
|
||||
SELECT l.*, t.brand, t.model, t.serial_number
|
||||
FROM loans l
|
||||
JOIN tablets t ON l.tablet_id = t.id
|
||||
WHERE l.student_id = ?
|
||||
ORDER BY l.loan_date DESC
|
||||
''', (student_id,))
|
||||
loans = cursor.fetchall()
|
||||
|
||||
return render_template('student_detail.html',
|
||||
student=student,
|
||||
assigned_tablets=assigned_tablets,
|
||||
assigned_devices=assigned_devices,
|
||||
loans=loans)
|
||||
|
||||
|
||||
@app.route('/edit_student/<int:student_id>', methods=['GET', 'POST'])
|
||||
def edit_student(student_id):
|
||||
"""Edit a student"""
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM students WHERE id = ?", (student_id,))
|
||||
student = cursor.fetchone()
|
||||
|
||||
if not student:
|
||||
flash('Error: Student not found!', 'error')
|
||||
return redirect(url_for('list_students'))
|
||||
|
||||
if request.method == 'POST':
|
||||
order_number = request.form.get('order_number')
|
||||
cial_code = request.form.get('cial_code', '').strip()
|
||||
nif_nie_passport = request.form.get('nif_nie_passport', '').strip()
|
||||
registration_number = request.form.get('registration_number')
|
||||
file_number = request.form.get('file_number', '').strip()
|
||||
first_name = request.form.get('first_name', '').strip()
|
||||
last_name = request.form.get('last_name', '').strip()
|
||||
full_name = f"{last_name}, {first_name}" if last_name and first_name else request.form.get('full_name', '').strip()
|
||||
birth_date = request.form.get('birth_date', '').strip()
|
||||
gender = request.form.get('gender', '').strip()
|
||||
study_group = request.form.get('study_group', '').strip()
|
||||
|
||||
# Validate required fields
|
||||
if not cial_code:
|
||||
flash('Error: CIAL code is required!', 'error')
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
if not first_name:
|
||||
flash('Error: First name is required!', 'error')
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
if not last_name:
|
||||
flash('Error: Last name is required!', 'error')
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
# Validate gender
|
||||
if gender and gender.upper() not in ['M', 'F', 'O']:
|
||||
flash('Error: Invalid gender! Must be M, F, or O.', 'error')
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
# Validate date format
|
||||
if birth_date:
|
||||
try:
|
||||
datetime.strptime(birth_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
flash('Error: Invalid date format! Use YYYY-MM-DD.', 'error')
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
UPDATE students SET
|
||||
order_number = ?,
|
||||
cial_code = ?,
|
||||
nif_nie_passport = ?,
|
||||
registration_number = ?,
|
||||
file_number = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
full_name = ?,
|
||||
birth_date = ?,
|
||||
gender = ?,
|
||||
study_group = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (order_number, cial_code, nif_nie_passport or None, registration_number,
|
||||
file_number or None, first_name, last_name, full_name,
|
||||
birth_date or None, gender or None, study_group or None, student_id))
|
||||
conn.commit()
|
||||
flash('Student updated successfully!', 'success')
|
||||
return redirect(url_for('student_detail', student_id=student_id))
|
||||
except sqlite3.IntegrityError as e:
|
||||
if 'UNIQUE constraint failed: students.cial_code' in str(e):
|
||||
flash('Error: CIAL code already exists!', 'error')
|
||||
elif 'UNIQUE constraint failed: students.nif_nie_passport' in str(e):
|
||||
flash('Error: NIF/NIE/Passport already exists!', 'error')
|
||||
else:
|
||||
flash(f'Error: {str(e)}', 'error')
|
||||
|
||||
return render_template('edit_student.html', student=student)
|
||||
|
||||
|
||||
@app.route('/delete_student/<int:student_id>')
|
||||
def delete_student(student_id):
|
||||
"""Delete a student"""
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if student has active loans or assigned devices
|
||||
cursor.execute("SELECT COUNT(*) FROM loans WHERE student_id = ? AND status = 'active'", (student_id,))
|
||||
active_loans = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tablets WHERE assigned_to_student = ?", (student_id,))
|
||||
assigned_tablets = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM non_loanable_devices WHERE assigned_to_student = ?", (student_id,))
|
||||
assigned_devices = cursor.fetchone()[0]
|
||||
|
||||
if active_loans > 0 or assigned_tablets > 0 or assigned_devices > 0:
|
||||
flash('Error: Cannot delete student with active loans or assigned devices!', 'error')
|
||||
return redirect(url_for('student_detail', student_id=student_id))
|
||||
|
||||
cursor.execute("DELETE FROM students WHERE id = ?", (student_id,))
|
||||
conn.commit()
|
||||
flash('Student deleted successfully!', 'success')
|
||||
|
||||
return redirect(url_for('list_students'))
|
||||
|
||||
|
||||
@app.route('/import_students', methods=['GET', 'POST'])
|
||||
def import_students():
|
||||
"""Import students from CSV file"""
|
||||
if request.method == 'POST':
|
||||
# Check if file was uploaded
|
||||
if 'csv_file' not in request.files:
|
||||
flash('Error: No file uploaded!', 'error')
|
||||
return redirect(url_for('import_students'))
|
||||
|
||||
file = request.files['csv_file']
|
||||
|
||||
if file.filename == '':
|
||||
flash('Error: No file selected!', 'error')
|
||||
return redirect(url_for('import_students'))
|
||||
|
||||
if not file.filename.lower().endswith('.csv'):
|
||||
flash('Error: Please upload a CSV file!', 'error')
|
||||
return redirect(url_for('import_students'))
|
||||
|
||||
# Save uploaded file temporarily
|
||||
temp_filename = file.filename if file.filename else 'import.csv'
|
||||
temp_path = os.path.join('/tmp', temp_filename)
|
||||
file.save(temp_path)
|
||||
|
||||
# Run import script
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['python3', 'scripts/import_students.py', temp_path, '--database', DATABASE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
|
||||
# Clean up temp file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
if result.returncode == 0:
|
||||
flash('Students imported successfully!', 'success')
|
||||
else:
|
||||
flash(f'Error importing students: {result.stderr}', 'error')
|
||||
|
||||
return redirect(url_for('list_students'))
|
||||
|
||||
return render_template('import_students.html')
|
||||
|
||||
|
||||
@app.route('/return_tablet/<int:loan_id>')
|
||||
def return_tablet(loan_id):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue