- version : 0.4

- description : successfully implementing import from .csv file feature
هذا الالتزام موجود في:
2025-06-10 16:06:54 +03:00
الأصل 3441a430b8
التزام e1ff741007
2 ملفات معدلة مع 255 إضافات و172 حذوفات

278
app.py
عرض الملف

@@ -1,168 +1,232 @@
import sqlite3
import csv
import io
# The 'flash' import is no longer needed
from flask import Flask, render_template, request, redirect, url_for
import re
from flask import Flask, render_template, request, redirect, url_for, flash
# Create a Flask application instance
# --- App Setup ---
app = Flask(__name__)
# The 'secret_key' is no longer required as we are not using flash/sessions
# app.secret_key = 'your_super_secret_key_12345'
app.secret_key = 'your_super_secret_key_12345'
app.config['PHONE_REGEX'] = re.compile(r'^09\d{8}$') # Syrian phone format
# --- Database Functions ---
def get_db_connection():
"""Creates and returns a connection to the SQLite database."""
conn = sqlite3.connect('students.db')
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""Initializes the database. THIS IS ONLY RUN MANUALLY from the command line."""
conn = get_db_connection()
with conn:
with get_db_connection() as conn:
conn.execute('DROP TABLE IF EXISTS students')
conn.execute('''
CREATE TABLE students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_name TEXT NOT NULL,
age INTEGER NOT NULL,
age INTEGER NOT NULL CHECK(age BETWEEN 5 AND 25),
parent_name TEXT NOT NULL,
parent_phone_1 TEXT NOT NULL,
parent_phone_2 TEXT,
student_phone TEXT,
parent_phone_1 TEXT NOT NULL CHECK(length(parent_phone_1) = 10),
parent_phone_2 TEXT CHECK(length(parent_phone_2) = 10 OR parent_phone_2 IS NULL),
student_phone TEXT CHECK(length(student_phone) = 10 OR student_phone IS NULL),
grade TEXT NOT NULL,
school_name TEXT NOT NULL,
address TEXT NOT NULL,
memorizing TEXT NOT NULL
)
''')
print("Database initialized and 'students' table created with the new schema.")
# Add indexes for faster search
conn.execute('CREATE INDEX idx_student_name ON students(student_name)')
conn.execute('CREATE INDEX idx_parent_phone ON students(parent_phone_1)')
print("Database initialized with schema constraints and indexes")
@app.cli.command('init-db')
def init_db_command():
"""Clears existing data and creates new tables."""
init_db()
print("Database initialized successfully")
# --- Validation Utilities ---
def validate_phone(phone):
return bool(app.config['PHONE_REGEX'].match(phone)) if phone else True
def validate_student_data(form_data, is_csv=False):
errors = []
required_fields = ['student_name', 'age', 'parent_name',
'parent_phone_1', 'grade', 'school_name',
'address', 'memorizing']
# Check required fields
for field in required_fields:
if not form_data.get(field):
errors.append(f"حقل '{field}' مطلوب")
# Validate phone formats
phones = [
('parent_phone_1', form_data.get('parent_phone_1')),
('parent_phone_2', form_data.get('parent_phone_2')),
('student_phone', form_data.get('student_phone'))
]
for field, value in phones:
if value and not validate_phone(value):
errors.append(f"رقم الهاتف '{field}' غير صالح. يجب أن يكون 10 أرقام ويبدأ بـ 09")
# Validate age
try:
age = int(form_data.get('age', 0))
if not (5 <= age <= 25):
errors.append("العمر يجب أن يكون بين 5 و 25 سنة")
except ValueError:
errors.append("العمر يجب أن يكون رقماً صحيحاً")
return errors
# --- App Routes ---
@app.route('/')
def index():
"""Renders the main page and displays any messages passed in the URL."""
# Get the message from the URL query parameters
message = request.args.get('message', None)
category = request.args.get('category', None)
conn = get_db_connection()
students = conn.execute('SELECT * FROM students ORDER BY id').fetchall()
conn.close()
# Pass the message and students to the template
return render_template('index.html', students=students, message=message, category=category)
@app.route('/add', methods=['POST'])
def add_student():
"""Handles the form submission for adding a single new student."""
try:
student_name = request.form['student_name']
age = request.form['age']
parent_name = request.form['parent_name']
parent_phone_1 = request.form['parent_phone_1']
parent_phone_2 = request.form.get('parent_phone_2')
student_phone = request.form.get('student_phone')
grade = request.form['grade']
school_name = request.form['school_name']
address = request.form['address']
memorizing = request.form['memorizing']
with get_db_connection() as conn:
students = conn.execute('''
SELECT * FROM students
ORDER BY student_name ASC
''').fetchall()
return render_template('index.html', students=students)
except sqlite3.Error as e:
flash(f'خطأ في قاعدة البيانات: {str(e)}', 'danger')
return render_template('index.html', students=[])
if not all([student_name, age, parent_name, parent_phone_1, grade, school_name, address, memorizing]):
# Instead of flashing, redirect with a message
return redirect(url_for('index', message='الرجاء تعبئة جميع الحقول المطلوبة.', category='danger'))
@app.route('/add_student', methods=['POST'])
def add_student():
form_data = request.form
validation_errors = validate_student_data(form_data)
conn = get_db_connection()
if validation_errors:
for error in validation_errors:
flash(error, 'danger')
return redirect(url_for('index'))
try:
student_data = (
form_data['student_name'],
int(form_data['age']),
form_data['parent_name'],
form_data['parent_phone_1'],
form_data.get('parent_phone_2') or None,
form_data.get('student_phone') or None,
form_data['grade'],
form_data['school_name'],
form_data['address'],
form_data['memorizing']
)
with get_db_connection() as conn:
conn.execute('''
INSERT INTO students (student_name, age, parent_name, parent_phone_1, parent_phone_2, student_phone, grade, school_name, address, memorizing)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (student_name, age, parent_name, parent_phone_1, parent_phone_2, student_phone, grade, school_name, address, memorizing))
INSERT INTO students (
student_name, age, parent_name,
parent_phone_1, parent_phone_2,
student_phone, grade, school_name,
address, memorizing
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', student_data)
conn.commit()
conn.close()
message = 'تمت إضافة الطالب الجديد بنجاح!'
category = 'success'
flash('تمت إضافة الطالب بنجاح!', 'success')
return redirect(url_for('index'))
except sqlite3.IntegrityError as e:
flash(f'خطأ في قاعدة البيانات: {str(e)}', 'danger')
except Exception as e:
message = f'حدث خطأ أثناء إضافة الطالب: {e}'
category = 'danger'
flash(f'خطأ غير متوقع: {str(e)}', 'danger')
return redirect(url_for('index', message=message, category=category))
return redirect(url_for('index'))
@app.route('/import', methods=['POST'])
@app.route('/import_csv', methods=['POST'])
def import_csv():
"""Handles CSV import and redirects with a status message in the URL."""
if 'file' not in request.files or request.files['file'].filename == '':
return redirect(url_for('index', message='لم يتم اختيار أي ملف.', category='danger'))
if 'file' not in request.files:
flash('لم يتم تقديم ملف', 'danger')
return redirect(url_for('index'))
file = request.files['file']
if file.filename == '':
flash('لم يتم اختيار ملف', 'warning')
return redirect(url_for('index'))
if not file.filename.lower().endswith('.csv'):
flash('صيغة الملف غير مدعومة. يجب أن يكون CSV', 'danger')
return redirect(url_for('index'))
if file and file.filename.endswith('.csv'):
students_to_add = [] # This is our "2D vector"
try:
stream = io.StringIO(file.stream.read().decode("utf-8-sig"), newline=None)
stream = io.TextIOWrapper(file.stream, encoding='utf-8-sig')
csv_reader = csv.reader(stream)
valid_rows = []
row_errors = []
# First, read all rows from CSV into the students_to_add list
for i, row in enumerate(csv_reader, start=1):
for i, row in enumerate(csv_reader, 1):
if len(row) != 10:
msg = f'خطأ في ملف CSV في السطر رقم {i}: يجب أن يحتوي السطر على 10 أعمدة.'
return redirect(url_for('index', message=msg, category='danger'))
try:
# Validate and prepare the row data
student_data = (
row[0], int(row[1]), row[2], row[3],
row[4] if row[4] else None, row[5] if row[5] else None,
row[6], row[7], row[8], row[9]
)
students_to_add.append(student_data)
except ValueError:
msg = f"خطأ في ملف CSV في السطر رقم {i}: يجب أن يكون العمر (العمود الثاني) رقماً."
return redirect(url_for('index', message=msg, category='danger'))
row_errors.append(f'السطر {i}: عدد الأعمدة غير صحيح (10 مطلوبة)')
continue
# If the list was successfully built, proceed to add to the database
if students_to_add:
conn = get_db_connection()
# Use a try/except block for the database operation
try:
# Now, loop through the "2D vector" and add each student one by one
for student in students_to_add:
conn.execute('''
INSERT INTO students (student_name, age, parent_name, parent_phone_1, parent_phone_2, student_phone, grade, school_name, address, memorizing)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', student)
# Map CSV columns to form fields
student_data = {
'student_name': row[0].strip(),
'age': row[1].strip(),
'parent_name': row[2].strip(),
'parent_phone_1': row[3].strip(),
'parent_phone_2': row[4].strip(),
'student_phone': row[5].strip(),
'grade': row[6].strip(),
'school_name': row[7].strip(),
'address': row[8].strip(),
'memorizing': row[9].strip()
}
# If all insertions succeed, commit them to the database
# Validate row
errors = validate_student_data(student_data, is_csv=True)
if errors:
row_errors.append(f'السطر {i}: {"; ".join(errors)}')
continue
# Prepare for insertion
valid_rows.append((
student_data['student_name'],
int(student_data['age']),
student_data['parent_name'],
student_data['parent_phone_1'],
student_data['parent_phone_2'] or None,
student_data['student_phone'] or None,
student_data['grade'],
student_data['school_name'],
student_data['address'],
student_data['memorizing']
))
# Process validation results
if row_errors:
flash(f'تم العثور على أخطاء في {len(row_errors)} سطراً', 'warning')
for error in row_errors[:5]: # Show first 5 errors
flash(error, 'danger')
if len(row_errors) > 5:
flash(f'...و {len(row_errors)-5} أخطاء إضافية', 'danger')
if valid_rows:
with get_db_connection() as conn:
conn.executemany('''
INSERT INTO students (
student_name, age, parent_name,
parent_phone_1, parent_phone_2,
student_phone, grade, school_name,
address, memorizing
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', valid_rows)
conn.commit()
message = f'تم استيراد وإضافة {len(students_to_add)} طالب بنجاح!'
category = 'success'
except sqlite3.Error as e:
# If any error occurs during insertion, the transaction is rolled back automatically
message = f'حدث خطأ في قاعدة البيانات أثناء الإضافة: {e}'
category = 'danger'
finally:
# Always close the connection
conn.close()
flash(f'تم استيراد {len(valid_rows)} طالب بنجاح', 'success')
else:
message = 'ملف CSV فارغ أو لا يمكن معالجته.'
category = 'warning'
flash('لم يتم استيراد أي سجلات', 'warning')
except csv.Error as e:
flash(f'خطأ في معالجة CSV: {str(e)}', 'danger')
except Exception as e:
message = f'حدث خطأ غير متوقع أثناء عملية الاستيراد: {e}'
category = 'danger'
return redirect(url_for('index', message=message, category=category))
else:
return redirect(url_for('index', message='صيغة الملف غير صالحة. الرجاء رفع ملف .csv فقط.', category='danger'))
flash(f'خطأ غير متوقع: {str(e)}', 'danger')
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)

عرض الملف

@@ -4,23 +4,27 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>بيانات الطلاب</title>
<!-- Tailwind CSS for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts: Cairo for Arabic -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Cairo', sans-serif; }
/* Style for disabled buttons */
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-6xl">
<!-- Display Messages passed via URL parameters -->
{% if message and category %}
<div class="mb-6 space-y-3">
<div class="mb-6 space-y-3" aria-live="polite" aria-atomic="true">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% set bg_color = 'bg-blue-100 border-blue-500 text-blue-700' %}
{% if category == 'success' %}{% set bg_color = 'bg-green-100 border-green-500 text-green-700' %}{% endif %}
{% if category == 'danger' %}{% set bg_color = 'bg-red-100 border-red-500 text-red-700' %}{% endif %}
@@ -28,19 +32,19 @@
<div class="{{ bg_color }} border-r-4 border-l-0 p-4 rounded-md shadow" role="alert">
<p class="font-bold">{{ message }}</p>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Header -->
<header class="text-center mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900">نظام إدارة الطلاب</h1>
<p class="text-gray-600 mt-2">أدخل بيانات الطلاب يدوياً أو قم باستيراد ملف CSV.</p>
</header>
<!-- Manual Add Student Form (Initially Hidden) -->
<div id="add-student-form" class="bg-white p-8 rounded-xl shadow-lg mb-8 hidden">
<div id="add-student-section" class="bg-white p-8 rounded-xl shadow-lg mb-8 hidden">
<h2 class="text-2xl font-semibold mb-6 text-gray-800 border-b pb-4">إضافة طالب جديد</h2>
<form action="{{ url_for('add_student') }}" method="POST">
<form id="add-student-form" action="{{ url_for('add_student') }}" method="POST">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
<div>
<label for="student_name" class="block text-sm font-medium text-gray-700 mb-1">اسم الطالب</label>
@@ -74,24 +78,23 @@
<label for="school_name" class="block text-sm font-medium text-gray-700 mb-1">اسم المدرسة</label>
<input type="text" name="school_name" id="school_name" placeholder="مثال: مدرسة المستقبل" required class="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<div class="md:col-span-2">
<label for="address" class="block text-sm font-medium text-gray-700 mb-1">العنوان</label>
<input type="text" name="address" id="address" placeholder="الشارع، المدينة" required class="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<div class="md:col-span-2">
<label for="memorizing" class="block text-sm font-medium text-gray-700 mb-1">المحفوظات</label>
<input type="text" name="memorizing" id="memorizing" placeholder="مثال: القرآن، جزء عم" required class="w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="mt-8 text-left">
<button type="submit" class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
<button type="submit" id="submit-add-button" class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
حفظ بيانات الطالب
</button>
</div>
</form>
</div>
<!-- CSV Import Section -->
<div class="bg-white p-6 rounded-xl shadow-lg mb-8">
<h2 class="text-2xl font-semibold mb-4 text-gray-800 border-b pb-4">استيراد من ملف CSV</h2>
<div class="bg-yellow-50 border border-yellow-200 text-yellow-800 text-sm p-4 rounded-lg mt-4">
@@ -105,26 +108,21 @@
</div>
<div class="mt-6 text-left">
<form id="import-form" action="{{ url_for('import_csv') }}" method="POST" enctype="multipart/form-data">
<!-- Hidden file input -->
<input type="file" name="file" id="csv-file-input" class="hidden" required accept=".csv">
<!-- Button to trigger file selection -->
<button type="button" id="choose-file-button" class="px-8 py-3 bg-green-600 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all">
<button type="button" id="choose-file-button" class="px-6 py-2 bg-green-600 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all">
اختر ملف
</button>
<!-- Span to display the selected file name -->
<span id="file-name-display" class="hidden ml-4 text-gray-700"></span>
<!-- Submit button, initially hidden -->
<button type="submit" id="submit-import-button" class="hidden px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
<button type="submit" id="submit-import-button" class="hidden px-6 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
تأكيد ورفع الملف
</button>
</form>
</div>
</div>
<!-- Student List Table -->
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-center mb-4 pb-4 border-b">
<h2 class="text-2xl font-semibold text-gray-800">الطلاب المسجلون</h2>
@@ -168,7 +166,7 @@
</tr>
{% else %}
<tr>
<td colspan="9" class="text-center py-6 text-gray-500">لم تتم إضافة أي طالب بعد.</td>
<td colspan="10" class="text-center py-6 text-gray-500">لم تتم إضافة أي طالب بعد.</td>
</tr>
{% endfor %}
</tbody>
@@ -182,35 +180,56 @@
// Get references to all necessary elements
const csvFileInput = document.getElementById('csv-file-input');
const chooseFileButton = document.getElementById('choose-file-button');
const submitImportButton = document.getElementById('submit-import-button');
const fileNameDisplay = document.getElementById('file-name-display');
const submitImportButton = document.getElementById('submit-import-button'); // Added for clarity
const addManuallyButton = document.getElementById('add-manually-button');
const addStudentSection = document.getElementById('add-student-section');
const addStudentForm = document.getElementById('add-student-form');
const importForm = document.getElementById('import-form');
const submitAddButton = document.getElementById('submit-add-button'); // Added for clarity
// When the "Choose File" button is clicked, trigger the hidden file input
// When the "Choose File" button is clicked, it programmatically clicks the hidden file input.
chooseFileButton.addEventListener('click', () => {
csvFileInput.click();
});
// When a file is selected in the hidden input...
// When a file is selected using the file input...
csvFileInput.addEventListener('change', () => {
if (csvFileInput.files.length > 0) {
// Get the name of the selected file
const fileName = csvFileInput.files[0].name;
fileNameDisplay.textContent = `الملف المختار: ${fileName}`;
// Show the file name and the submit button
// Show the file name display and the submit button
fileNameDisplay.classList.remove('hidden');
submitImportButton.classList.remove('hidden');
// Hide the original "Choose File" button
// Hide the original "Choose File" button to avoid confusion
chooseFileButton.classList.add('hidden');
} else {
// If no file is selected (e.g., user cancels file dialog)
fileNameDisplay.classList.add('hidden');
submitImportButton.classList.add('hidden');
chooseFileButton.classList.remove('hidden');
}
});
// Toggle the visibility of the "Add New Student" form
addManuallyButton.addEventListener('click', () => {
addStudentForm.classList.toggle('hidden');
addStudentSection.classList.toggle('hidden');
});
// --- Prevent Double Submissions ---
// For the CSV Import Form
importForm.addEventListener('submit', () => {
submitImportButton.disabled = true;
submitImportButton.textContent = 'جاري الرفع...'; // "Uploading..."
});
// For the Manual Add Student Form
addStudentForm.addEventListener('submit', () => {
submitAddButton.disabled = true;
submitAddButton.textContent = 'جاري الحفظ...'; // "Saving..."
});
</script>
</body>