diff --git a/.gitignore b/.gitignore index 82f9275..2cc90c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.idea +output.xlsx + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 2534482..e3fc260 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # QR-code-attendance + Just a QR code attendance demo app for my sister's representation + +## How to use? + +1. Install python3: + Go to [python.org](https://www.python.org/downloads/) and download the latest version of python3. +2. Clone the repository + ```bash + git clone https://github.com/tiennm99/qr-code-attendance.git + ``` + Or download the zip file and extract it. +3. Run `setup.bat` to install the required packages. +4. Run `run.bat` to start the server. + +# Note + +**These code for educational purposes only. It is not secure and not optimized for production use.** + +_Good luck, sis!_ diff --git a/app.py b/app.py new file mode 100644 index 0000000..1767c18 --- /dev/null +++ b/app.py @@ -0,0 +1,193 @@ +from threading import Timer + +import psutil +import qrcode +from flask import Flask, render_template, request, jsonify, redirect, url_for, abort +import io +import base64 +import pandas as pd +from datetime import datetime +import os +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill +import webbrowser +from collections import deque +from functools import wraps + +app = Flask(__name__) + +PORT = 31009 +INPUT_EXCEL = 'input.xlsx' +OUTPUT_EXCEL = 'output.xlsx' + +# Use a deque to store the last submissions +last_submissions = deque(maxlen=10) + + +def open_browser(): + webbrowser.open_new(f'http://localhost:{PORT}/private') + + +def get_ip_addresses(): + ip_addresses = [] + for interface, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == 2: # AF_INET (IPv4) + ip_addresses.append((interface, addr.address)) + return ip_addresses + + +def generate_qr_code(data): + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(data) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + return img + + +def initialize_output_excel(): + if not os.path.exists(INPUT_EXCEL): + raise FileNotFoundError(f"{INPUT_EXCEL} not found. Please provide the input Excel file.") + + df = pd.read_excel(INPUT_EXCEL) + df['Attended'] = '' + df['Submit Time'] = '' + df['IP'] = '' + df['User Agent'] = '' + df.to_excel(OUTPUT_EXCEL, index=False) + + +def localhost_only(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if request.remote_addr != '127.0.0.1': + abort(403) # Forbidden + return f(*args, **kwargs) + + return decorated_function + + +@app.route('/private') +@localhost_only +def index(): + ip_addresses = get_ip_addresses() + return render_template('index.html', ip_addresses=ip_addresses, last_submissions=list(last_submissions)) + + +@app.route('/private/generate_qr', methods=['POST']) +@localhost_only +def generate_qr(): + selected_ip = request.json.get('ip') + full_url = f"http://{selected_ip}:{PORT}/public/attendance" + qr_img = generate_qr_code(full_url) + img_buffer = io.BytesIO() + qr_img.save(img_buffer, format='PNG') + img_buffer.seek(0) + qr_code = base64.b64encode(img_buffer.getvalue()).decode() + return jsonify({'qr_code': qr_code, 'full_url': full_url}) + + +@app.route('/public/attendance', methods=['GET', 'POST']) +def attendance(): + if request.method == 'POST': + student_id = request.form['student_id'] + ip_address = request.remote_addr + user_agent = request.user_agent.string + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + result = update_attendance(student_id, timestamp, ip_address, user_agent) + if result == 'duplicate': + return redirect(url_for('attendance_error', error='duplicate')) + elif result == 'student_not_found': + return redirect(url_for('attendance_error', error='not_found')) + return redirect(url_for('attendance_success')) + return render_template('attendance_form.html') + + +@app.route('/attendance_success') +def attendance_success(): + return render_template('attendance_success.html') + + +@app.route('/attendance_error') +def attendance_error(): + error = request.args.get('error', 'unknown') + if error == 'duplicate': + message = "Your attendance has been recorded, but it appears to be a duplicate submission." + elif error == 'not_found': + message = "Student ID not found. Please check your ID and try again." + else: + message = "An unknown error occurred. Please try again later." + return render_template('attendance_error.html', message=message) + + +def update_attendance(student_id, timestamp, ip_address, user_agent): + wb = load_workbook(OUTPUT_EXCEL) + ws = wb.active + + student_row = None + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + if str(row[0].value) == str(student_id): + student_row = row + break + + if student_row is None: + wb.close() + return 'student_not_found' + + row_index = student_row[0].row + is_duplicate = False + duplicate_with = None + + # Check for duplicate IP or User Agent + for other_row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=5, max_col=6): + if other_row[0].value == ip_address or other_row[1].value == user_agent: + is_duplicate = True + duplicate_with = ws.cell(row=other_row[0].row, column=1).value + break + + # Check if this student has already attended + if ws.cell(row=row_index, column=3).value: + is_duplicate = True + + # Always save submission details + ws.cell(row=row_index, column=4, value=timestamp) + ws.cell(row=row_index, column=5, value=ip_address) + ws.cell(row=row_index, column=6, value=user_agent) + + if is_duplicate: + duplicate_cell = ws.cell(row=row_index, column=3) + if duplicate_with: + duplicate_cell.value = f"Duplicated with {duplicate_with}" + else: + duplicate_cell.value = "Duplicate submission" + duplicate_cell.font = Font(color="FF0000") + duplicate_cell.fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") + else: + # Mark attendance + ws.cell(row=row_index, column=3, value='X') + + # Add to last 5 submissions + student_name = ws.cell(row=row_index, column=2).value + last_submissions.appendleft({ + 'Student ID': student_id, + 'Student Name': student_name, + 'Submit Time': timestamp + }) + + wb.save(OUTPUT_EXCEL) + wb.close() + return 'duplicate' if is_duplicate else 'success' + + +@app.route('/private/get_last_submissions') +@localhost_only +def get_last_submissions(): + return jsonify(list(last_submissions)) + + +if __name__ == '__main__': + if not os.path.exists(OUTPUT_EXCEL): + initialize_output_excel() + Timer(1, open_browser).start() + app.run(host='0.0.0.0', port=PORT, debug=True) diff --git a/input.xlsx b/input.xlsx new file mode 100644 index 0000000..a63ce56 Binary files /dev/null and b/input.xlsx differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..da9a239 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +blinker==1.8.2 +click==8.1.7 +colorama==0.4.6 +et-xmlfile==1.1.0 +Flask==3.0.3 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +netifaces==0.11.0 +numpy==2.0.1 +openpyxl==3.1.5 +pandas==2.2.2 +pillow==10.4.0 +psutil==6.0.0 +pypng==0.20220715.0 +python-dateutil==2.9.0.post0 +pytz==2024.1 +qrcode==7.4.2 +six==1.16.0 +typing_extensions==4.12.2 +tzdata==2024.1 +Werkzeug==3.0.3 diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..0bbbf73 --- /dev/null +++ b/run.bat @@ -0,0 +1,9 @@ +@echo off +echo Starting the application... +python app.py +if %errorlevel% neq 0 ( + echo Error: Failed to run the application. + pause + exit /b %errorlevel% +) +pause diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..516b19c --- /dev/null +++ b/setup.bat @@ -0,0 +1,10 @@ +@echo off +echo Installing requirements... +pip install -r requirements.txt +if %errorlevel% neq 0 ( + echo Error: Failed to install requirements. + pause + exit /b %errorlevel% +) +echo Requirements installed successfully. +pause diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..eb3db59 --- /dev/null +++ b/static/script.js @@ -0,0 +1,64 @@ +document.addEventListener('DOMContentLoaded', function() { + const form = document.getElementById('ip-form'); + const qrCodeContainer = document.getElementById('qr-code-container'); + const submissionsTable = document.getElementById('submissions-table'); + + form.addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(form); + const selectedIp = formData.get('ip'); + + fetch('/private/generate_qr', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ip: selectedIp }), + }) + .then(response => response.json()) + .then(data => { + qrCodeContainer.innerHTML = ` +
Error generating QR code. Please try again.
'; + }); + }); + + function updateSubmissions() { + fetch('/private/get_last_submissions') + .then(response => response.json()) + .then(data => { + let tableHtml = ` + +Thank you for submitting your attendance.
+| Student ID | +Student Name | +Submit Time | +
|---|---|---|
| {{ submission['Student ID'] }} | +{{ submission['Student Name'] }} | +{{ submission['Submit Time'] }} | +