From 83cce0ae16b9906fdd78ed32fcd158026cb88770 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 10 Aug 2024 12:11:29 +0700 Subject: [PATCH] (Add) init --- .gitignore | 3 + README.md | 19 +++ app.py | 193 ++++++++++++++++++++++++++++++ input.xlsx | Bin 0 -> 8616 bytes requirements.txt | 22 ++++ run.bat | 9 ++ setup.bat | 10 ++ static/script.js | 64 ++++++++++ templates/attendance_error.html | 16 +++ templates/attendance_form.html | 22 ++++ templates/attendance_success.html | 15 +++ templates/index.html | 45 +++++++ 12 files changed, 418 insertions(+) create mode 100644 app.py create mode 100644 input.xlsx create mode 100644 requirements.txt create mode 100644 run.bat create mode 100644 setup.bat create mode 100644 static/script.js create mode 100644 templates/attendance_error.html create mode 100644 templates/attendance_form.html create mode 100644 templates/attendance_success.html create mode 100644 templates/index.html 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 0000000000000000000000000000000000000000..a63ce566bdf038747fcbf35b82f8b21b1aeab9bf GIT binary patch literal 8616 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 R0B{ra@kePl@!cQ4{sWyb^4kCa literal 0 HcmV?d00001 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 = ` +

QR Code for ${data.full_url}

+ QR Code + `; + }) + .catch(error => { + console.error('Error:', error); + qrCodeContainer.innerHTML = '

Error generating QR code. Please try again.

'; + }); + }); + + function updateSubmissions() { + fetch('/private/get_last_submissions') + .then(response => response.json()) + .then(data => { + let tableHtml = ` + + + Student ID + Student Name + Submit Time + + + + `; + data.forEach(submission => { + tableHtml += ` + + ${submission['Student ID']} + ${submission['Student Name']} + ${submission['Submit Time']} + + `; + }); + tableHtml += ''; + submissionsTable.innerHTML = tableHtml; + }) + .catch(error => console.error('Error:', error)); + } + + // Update submissions every 5 seconds + setInterval(updateSubmissions, 5000); + // Initial update + updateSubmissions(); +}); diff --git a/templates/attendance_error.html b/templates/attendance_error.html new file mode 100644 index 0000000..08db5ad --- /dev/null +++ b/templates/attendance_error.html @@ -0,0 +1,16 @@ + + + + + + Attendance Submission Error + + + +
+ + diff --git a/templates/attendance_form.html b/templates/attendance_form.html new file mode 100644 index 0000000..ef1fe78 --- /dev/null +++ b/templates/attendance_form.html @@ -0,0 +1,22 @@ + + + + + + Student Attendance Form + + + +
+

Student Attendance Form

+
+
+ + +
+ +
+
+ + + diff --git a/templates/attendance_success.html b/templates/attendance_success.html new file mode 100644 index 0000000..ae6be1c --- /dev/null +++ b/templates/attendance_success.html @@ -0,0 +1,15 @@ + + + + + + Attendance Submitted Successfully + + + +
+

Attendance Submitted Successfully

+

Thank you for submitting your attendance.

+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e65926f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,45 @@ + + + + + + QR Code Attendance + + + +
+

QR Code Attendance

+
+ + +
+
+ +

Last Submissions

+ + + + + + + + + + {% for submission in last_submissions %} + + + + + + {% endfor %} + +
Student IDStudent NameSubmit Time
{{ submission['Student ID'] }}{{ submission['Student Name'] }}{{ submission['Submit Time'] }}
+
+ + + +