mirror of
https://github.com/tiennm99/qr-code-attendance.git
synced 2026-08-13 02:52:05 +00:00
(Add) init
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
.idea
|
||||
output.xlsx
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -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!_
|
||||
|
||||
@@ -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)
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = `
|
||||
<h2 class="mb-3">QR Code for ${data.full_url}</h2>
|
||||
<img src="data:image/png;base64,${data.qr_code}" alt="QR Code" class="img-fluid">
|
||||
`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
qrCodeContainer.innerHTML = '<p class="text-danger">Error generating QR code. Please try again.</p>';
|
||||
});
|
||||
});
|
||||
|
||||
function updateSubmissions() {
|
||||
fetch('/private/get_last_submissions')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let tableHtml = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Student ID</th>
|
||||
<th>Student Name</th>
|
||||
<th>Submit Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
data.forEach(submission => {
|
||||
tableHtml += `
|
||||
<tr>
|
||||
<td>${submission['Student ID']}</td>
|
||||
<td>${submission['Student Name']}</td>
|
||||
<td>${submission['Submit Time']}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
tableHtml += '</tbody>';
|
||||
submissionsTable.innerHTML = tableHtml;
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
// Update submissions every 5 seconds
|
||||
setInterval(updateSubmissions, 5000);
|
||||
// Initial update
|
||||
updateSubmissions();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Attendance Submission Error</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5 text-center">
|
||||
<h1 class="mb-4">Attendance Submission Error</h1>
|
||||
<p class="lead">{{ message }}</p>
|
||||
<a href="{{ url_for('attendance') }}" class="btn btn-primary">Try Again</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Student Attendance Form</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="text-center mb-4">Student Attendance Form</h1>
|
||||
<form action="{{ url_for('attendance') }}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="student_id" class="form-label">Student ID</label>
|
||||
<input type="text" class="form-control" id="student_id" name="student_id" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit Attendance</button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Attendance Submitted Successfully</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5 text-center">
|
||||
<h1 class="mb-4">Attendance Submitted Successfully</h1>
|
||||
<p class="lead">Thank you for submitting your attendance.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Code Attendance</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="text-center mb-4">QR Code Attendance</h1>
|
||||
<form id="ip-form">
|
||||
<select class="form-select mb-3" id="ip-select" name="ip">
|
||||
{% for interface, ip in ip_addresses %}
|
||||
<option value="{{ ip }}">{{ interface }}: {{ ip }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Generate QR Code</button>
|
||||
</form>
|
||||
<div id="qr-code-container" class="mt-4 text-center"></div>
|
||||
|
||||
<h2 class="mt-5">Last Submissions</h2>
|
||||
<table id="submissions-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Student ID</th>
|
||||
<th>Student Name</th>
|
||||
<th>Submit Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for submission in last_submissions %}
|
||||
<tr>
|
||||
<td>{{ submission['Student ID'] }}</td>
|
||||
<td>{{ submission['Student Name'] }}</td>
|
||||
<td>{{ submission['Submit Time'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user