diff --git a/app.py b/app.py index 344912c..82ca45a 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ import sqlite3 import csv import io import re -import os # Import the os module +import os from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory import datetime @@ -12,20 +12,17 @@ app.secret_key = 'your_super_secret_key_12345' app.config['PHONE_REGEX'] = re.compile(r'^09\d{8}$') # Syrian phone format # Define the path to the database folder -# This creates a path like 'your_project/databases/students.db' -DATABASE_FOLDER = os.path.join(app.root_path, 'databases') # Use app.root_path for reliable directory +DATABASE_FOLDER = os.path.join(app.root_path, 'databases') DATABASE_FILE = os.path.join(DATABASE_FOLDER, 'students.db') # --- Database Functions --- def get_db_connection(): - # Ensure the database folder exists before connecting os.makedirs(DATABASE_FOLDER, exist_ok=True) - conn = sqlite3.connect(DATABASE_FILE) # Connect to the specified path + conn = sqlite3.connect(DATABASE_FILE) conn.row_factory = sqlite3.Row return conn def init_db(): - # Ensure the database folder exists before creating the DB file os.makedirs(DATABASE_FOLDER, exist_ok=True) with get_db_connection() as conn: conn.execute('DROP TABLE IF EXISTS students') @@ -47,7 +44,6 @@ def init_db(): points INTEGER DEFAULT 0 NOT NULL ) ''') - # Add indexes for faster search conn.execute('CREATE INDEX idx_student_name ON students(student_name)') conn.execute('CREATE INDEX idx_parent_name ON students(parent_name)') print("Database initialized with schema constraints and indexes") @@ -352,7 +348,7 @@ def import_csv(): flash('لم يتم استيراد أي سجلات', 'warning') except csv.Error as e: - flash(f'خطأ في معالجة CSV: {str(e)}', 'danger') + flash(f'خطأ في معالشة CSV: {str(e)}', 'danger') except Exception as e: flash(f'خطأ غير متوقع: {str(e)}', 'danger') @@ -369,75 +365,68 @@ def record(): @app.route('/points', methods=['GET', 'POST']) def points(): if request.method == 'POST': - # Check for 'all_students' checkbox first - apply_to_all_students = 'all_students' in request.form - student_id = request.form.get('student_id') # This will be empty if 'all_students' is checked + # Now student_id will be a list of selected IDs from the checkboxes + selected_student_ids = request.form.getlist('student_id') point_amount_str = request.form.get('point_amount') - operation = request.form.get('operation') # 'add' or 'remove' + operation = request.form.get('operation') - if (not apply_to_all_students and not student_id) or not point_amount_str or not operation: - flash('الرجاء اختيار طالب واحد على الأقل (أو كل الطلاب) وتعبئة جميع الحقول المطلوبة.', 'danger') + if not selected_student_ids or not point_amount_str or not operation: + flash('الرجاء اختيار طالب واحد على الأقل وتعبئة جميع الحقول المطلوبة.', 'danger') return redirect(url_for('points')) try: point_amount = int(point_amount_str) - if point_amount <= 0: # Changed from < 0 to <= 0 + if point_amount <= 0: flash('الرجاء إدخال قيمة نقاط أكبر من صفر.', 'danger') return redirect(url_for('points')) with get_db_connection() as conn: - students_to_update = [] - if apply_to_all_students: - students_to_update = conn.execute('SELECT id, student_name, points FROM students').fetchall() - else: - # Fetch single student if not applying to all - student = conn.execute('SELECT id, student_name, points FROM students WHERE id = ?', (student_id,)).fetchone() - if student: - students_to_update.append(student) - else: - flash('الطالب المحدد غير موجود.', 'danger') - return redirect(url_for('points')) - - if not students_to_update: - flash('لا يوجد طلاب لتحديث نقاطهم.', 'warning') + # Ensure IDs are integers and unique + int_selected_ids = sorted(list(set([int(sid) for sid in selected_student_ids if sid.isdigit()]))) + if not int_selected_ids: + flash('لم يتم تحديد أي طالب صالح.', 'danger') return redirect(url_for('points')) - updated_count = 0 + placeholders = ','.join(['?'] * len(int_selected_ids)) + query = f"SELECT id, student_name, points FROM students WHERE id IN ({placeholders})" + students_to_update = conn.execute(query, int_selected_ids).fetchall() + + if not students_to_update: + flash('لم يتم العثور على أي طلاب مطابقين للاختيار.', 'danger') + return redirect(url_for('points')) + + updated_details = [] for student in students_to_update: current_points = student['points'] student_name = student['student_name'] new_points = current_points - applied_amount = point_amount # Amount actually applied for logging/messages if operation == 'add': new_points += point_amount - flash_message_prefix = f'تمت إضافة {point_amount} نقطة لـ {student_name}.' elif operation == 'remove': if current_points < point_amount: - applied_amount = current_points # Only remove what's available new_points = 0 # Cap at zero - flash_message_prefix = (f'لا يمكن خصم {point_amount} نقطة من {student_name} حيث يمتلك {current_points} نقطة فقط. ' - f'تم خصم {applied_amount} نقطة وتعيين النقاط إلى 0.') else: new_points -= point_amount - flash_message_prefix = f'تم خصم {point_amount} نقطة من {student_name}.' else: flash('عملية غير صالحة.', 'danger') return redirect(url_for('points')) conn.execute('UPDATE students SET points = ? WHERE id = ?', (new_points, student['id'])) - updated_count += 1 - # Flash message per student if not all, or accumulate for all - if not apply_to_all_students: - flash(f'{flash_message_prefix} النقاط الجديدة لـ {student_name}: {new_points}', 'success' if new_points >=0 else 'warning') # category based on points + updated_details.append(f"{student_name} (أصبح {new_points})") + conn.commit() - if apply_to_all_students: - total_students = len(students_to_update) - flash_op_text = "إضافة" if operation == "add" else "خصم" - flash(f'تم {flash_op_text} {point_amount} نقطة لـ {updated_count} طالب بنجاح.', 'success') - + flash_op_text = "إضافة" if operation == "add" else "خصم" + if len(updated_details) == 1: + flash(f'تمت عملية {flash_op_text} النقاط للطالب {updated_details[0].replace(" (أصبح", " والنقاط الجديدة")}.', 'success') + else: + flash_message_head = f'تمت عملية {flash_op_text} النقاط لـ {len(updated_details)} طلاب.' + flash_message_body = 'التفاصيل: ' + ', '.join(updated_details[:5]) + if len(updated_details) > 5: + flash_message_body += f'... والمزيد.' + flash(f'{flash_message_head} {flash_message_body}', 'success') except ValueError: flash('النقاط يجب أن تكون أرقاماً صحيحة.', 'danger') @@ -477,4 +466,4 @@ def delete_student(student_id): return redirect(url_for('index')) if __name__ == '__main__': - app.run(debug=True) + app.run(debug=True) \ No newline at end of file diff --git a/templates/points.html b/templates/points.html index e47f8a7..c6294b1 100644 --- a/templates/points.html +++ b/templates/points.html @@ -24,23 +24,20 @@

إدارة نقاط الطلاب

- -
- - -
+ + -
- - - + {# Replaced + +
+ +
@@ -111,14 +108,23 @@ const quickAddButtons = document.querySelectorAll('.quick-add-btn'); const searchStudentInput = document.getElementById('search_student_input'); - const studentSelect = document.getElementById('student_id'); - const studentOptions = Array.from(studentSelect.options); + const studentCheckboxList = document.getElementById('student_checkbox_list'); + const dummyStudentSelector = document.getElementById('dummy_student_selector'); // Hidden input to manage 'required' state - // New elements - const allStudentsCheckbox = document.getElementById('all_students_checkbox'); - const individualStudentSelectionDiv = document.getElementById('individual_student_selection'); + const selectAllStudentsCheckbox = document.getElementById('select_all_students_checkbox'); - // Function to perform a fuzzy match (same as before) + // Store all student data to generate checkboxes dynamically + // This array will hold objects like { id: 1, name: "Student A", points: 10 } + const allStudentData = [ + {% for student in students %} + { id: {{ student.id }}, name: "{{ student.student_name }}", points: {{ student.points }} }, + {% endfor %} + ]; + + // Store the state of checkboxes across filters + const selectedStudentIds = new Set(); // Use a Set for efficient ID tracking + + // Function to perform a fuzzy match function fuzzyMatch(pattern, text) { pattern = pattern.toLowerCase(); text = text.toLowerCase(); @@ -134,78 +140,116 @@ return patternIdx === pattern.length; } - // Function to filter dropdown options (updated to respect allStudentsCheckbox) - function filterStudentDropdown() { - if (allStudentsCheckbox.checked) { - return; // Do not filter if 'All Students' is checked - } - + // Function to render/filter the student checkboxes + function renderStudentCheckboxes() { const searchTerm = searchStudentInput.value.trim(); - studentSelect.innerHTML = ''; // Clear current options + studentCheckboxList.innerHTML = ''; // Clear current checkboxes - const defaultOption = document.createElement('option'); - defaultOption.value = ''; - defaultOption.textContent = 'اختر طالباً...'; - studentSelect.appendChild(defaultOption); + let studentsFound = 0; - if (searchTerm === '') { - studentOptions.forEach(option => { - if (option.value !== '') { // Re-add only actual student options - studentSelect.appendChild(option.cloneNode(true)); + allStudentData.forEach(student => { + const studentName = student.name; + const matchesSearch = searchTerm === '' || fuzzyMatch(searchTerm, studentName); + + if (matchesSearch) { + studentsFound++; + const checkboxId = `student_id_${student.id}`; + const div = document.createElement('div'); + div.className = 'flex items-center py-1'; // Add some padding + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.name = 'student_id'; // Important: all checkboxes have the same name + input.value = student.id; + input.id = checkboxId; + input.className = 'h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 cursor-pointer'; + + const label = document.createElement('label'); + label.htmlFor = checkboxId; + label.className = 'ml-2 block text-sm text-gray-900 cursor-pointer flex-1'; + label.textContent = `${student.name} (النقاط الحالية: ${student.points})`; + + // Restore selection state + if (selectedStudentIds.has(student.id)) { + input.checked = true; } - }); - } else { - studentOptions.forEach(option => { - const studentName = option.dataset.studentName; - if (option.value !== '' && studentName && fuzzyMatch(searchTerm, studentName)) { - studentSelect.appendChild(option.cloneNode(true)); - } - }); + + // Add event listener directly to the checkbox + input.addEventListener('change', (event) => { + if (event.target.checked) { + selectedStudentIds.add(student.id); + } else { + selectedStudentIds.delete(student.id); + } + // Update select all checkbox status + updateSelectAllCheckboxState(); + updateSubmitButtonState(); + }); + + div.appendChild(input); + div.appendChild(label); + studentCheckboxList.appendChild(div); + } + }); + + if (studentsFound === 0 && searchTerm !== '') { + studentCheckboxList.innerHTML = '

لا توجد نتائج بحث مطابقة.

'; } - // Ensure the selected value remains valid or reset if hidden - if (studentSelect.value !== '' && studentSelect.selectedOptions[0].parentElement !== studentSelect) { - studentSelect.value = ''; // Deselect if the current choice is now hidden - } - updateSubmitButtonState(); + + updateSelectAllCheckboxState(); // Update select all checkbox based on current filtered view + updateSubmitButtonState(); // Update submit button state } - // Function to update the submit button state (modified for all_students_checkbox) + // Function to update the "Select All" checkbox state + function updateSelectAllCheckboxState() { + const visibleCheckboxes = Array.from(studentCheckboxList.querySelectorAll('input[type="checkbox"]')); + if (visibleCheckboxes.length === 0) { + selectAllStudentsCheckbox.checked = false; + selectAllStudentsCheckbox.disabled = true; + } else { + selectAllStudentsCheckbox.disabled = false; + const allVisibleChecked = visibleCheckboxes.every(cb => cb.checked); + selectAllStudentsCheckbox.checked = allVisibleChecked; + } + } + + // Function to update the submit button state function updateSubmitButtonState() { - const studentSelected = studentSelect.value !== ''; - const allStudentsChecked = allStudentsCheckbox.checked; + // Now, we just check if any checkbox is selected (using our Set) + const studentsSelected = selectedStudentIds.size > 0; const amountEntered = pointAmountInput.value.trim() !== '' && parseInt(pointAmountInput.value) > 0; const operationSelected = operationInput.value !== ''; - // The button is enabled if: - // (a specific student is selected OR all students checkbox is checked) - // AND a valid amount is entered - // AND an operation (add/remove) is selected - if ((studentSelected || allStudentsChecked) && amountEntered && operationSelected) { + if (studentsSelected && amountEntered && operationSelected) { submitPointsBtn.disabled = false; submitPointsBtn.classList.remove('bg-gray-400', 'hover:bg-gray-500'); submitPointsBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); + dummyStudentSelector.removeAttribute('required'); // Allow submission } else { submitPointsBtn.disabled = true; submitPointsBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); submitPointsBtn.classList.add('bg-gray-400', 'hover:bg-gray-500'); + dummyStudentSelector.setAttribute('required', 'required'); // Prevent submission } } - // Event listener for All Students checkbox - allStudentsCheckbox.addEventListener('change', () => { - if (allStudentsCheckbox.checked) { - individualStudentSelectionDiv.classList.add('hidden'); // Hide individual selection - studentSelect.removeAttribute('required'); // No longer required if all students - studentSelect.value = ''; // Clear selection - searchStudentInput.value = ''; // Clear search input - filterStudentDropdown(); // Reset dropdown visually - } else { - individualStudentSelectionDiv.classList.remove('hidden'); // Show individual selection - studentSelect.setAttribute('required', 'required'); // Make dropdown required again - } + // Event listener for "Select All" checkbox + selectAllStudentsCheckbox.addEventListener('change', () => { + const isChecked = selectAllStudentsCheckbox.checked; + studentCheckboxList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { + checkbox.checked = isChecked; + // Manually update the selectedStudentIds set + if (isChecked) { + selectedStudentIds.add(parseInt(checkbox.value)); + } else { + selectedStudentIds.delete(parseInt(checkbox.value)); + } + }); updateSubmitButtonState(); }); + + // Event listeners for Add/Remove buttons addPointsBtn.addEventListener('click', () => { operationInput.value = 'add'; addPointsBtn.classList.add('bg-green-700'); @@ -220,6 +264,7 @@ updateSubmitButtonState(); }); + // Event listener for quick add buttons quickAddButtons.forEach(button => { button.addEventListener('click', () => { pointAmountInput.value = button.dataset.points; @@ -227,15 +272,17 @@ }); }); + // Event listener for manual input change pointAmountInput.addEventListener('input', updateSubmitButtonState); - studentSelect.addEventListener('change', updateSubmitButtonState); - searchStudentInput.addEventListener('input', filterStudentDropdown); - // Initial state check when the page loads - // Trigger checkbox change listener on load to set initial state - allStudentsCheckbox.dispatchEvent(new Event('change')); - updateSubmitButtonState(); + // Event listener for search input + searchStudentInput.addEventListener('input', renderStudentCheckboxes); + // Initial setup on page load + renderStudentCheckboxes(); // Render initial list of all students + updateSubmitButtonState(); // Set initial button state + + // Prevent double submission const pointsForm = document.getElementById('points-form'); if (pointsForm) { pointsForm.addEventListener('submit', () => {