test commit

This commit is contained in:
Xiao Furen 2025-08-05 06:15:47 +08:00
commit b8f2bbbadf
25 changed files with 890 additions and 0 deletions

206
.gitignore vendored Normal file
View file

@ -0,0 +1,206 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
#########
db.sqlite3.*

22
point_system/manage.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'point_system.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View file

View file

@ -0,0 +1,16 @@
"""
ASGI config for point_system project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'point_system.settings')
application = get_asgi_application()

View file

@ -0,0 +1,2 @@
from django.db import models

View file

@ -0,0 +1,128 @@
"""
Django settings for point_system project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-h-e*7!1gztzg6*m5k-oo-vp_y26jukv+%z-6ikw*f_n0dvm(mr'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'students.apps.StudentsConfig',
# 'point_system.apps.PointSystemConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'point_system.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'point_system.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-hant'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Taipei'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View file

@ -0,0 +1,25 @@
"""
URL configuration for point_system project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('students.urls')),
]

View file

@ -0,0 +1,16 @@
"""
WSGI config for point_system project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'point_system.settings')
application = get_wsgi_application()

25
point_system/students.csv Normal file
View file

@ -0,0 +1,25 @@
"學號
Student
ID","姓名
Name","年級
year","希望生
Hope admission
scheme students","減免
Tuition
waiver","弱勢_生輔組
Financial assistances
for disadvantaged
students","就學貸款
Loan",Email
D13K48007,李艾臻,1,'-,'-,'-,0,D13K48007@ntu.edu.tw;zaq4747@yahoo.com.tw
R13458003,林慕峰,1,'-,'-,'-,0,R13458003@ntu.edu.tw;brenton.lin.tw@gmail.com
R13458019,"林 樸",1,'-,'-,'-,0,R13458019@ntu.edu.tw;pu@johnsonandannie.com
R13K47020,劉書瑜,1,'-,'-,'-,0,R13K47020@ntu.edu.tw;freestyle770614@gmail.com
R13K47029,陳品辰,1,'-,'-,'-,0,R13K47029@ntu.edu.tw;sw1sw2sw3@gmail.com
R13K47031,"NANDHITHA SURULIANDI",1,'-,'-,'-,0,R13K47031@ntu.edu.tw;nandhithasuruliandi@gmail.com
D12K48020,傅冠豪,2,'-,'-,'-,0,D12K48020@ntu.edu.tw;haobbc@hotmail.com
R12458006,周宜葇,2,'-,通過,'-,0,R12458006@ntu.edu.tw;r12458006@g.ntu.edu.tw
R12458007,林育暄,2,'-,'-,'-,0,R12458007@ntu.edu.tw;sunny891228@gmail.com
R12458014,劉羽軒,2,'-,'-,'-,0,R12458014@ntu.edu.tw;liumilk613@gmail.com
R12458015,"ZOLNAMAR DORJSEMBE",2,'-,'-,'-,0,R12458015@ntu.edu.tw;zolnamar@gmail.com
R11458015,郭士榮,3,'-,'-,'-,0,R11458015@ntu.edu.tw;tonypaul57@gmail.com
1 學號 Student ID 姓名 Name 年級 year 希望生 Hope admission scheme’ students 減免 Tuition waiver 弱勢_生輔組 Financial assistances for disadvantaged students 就學貸款 Loan Email
2 D13K48007 李艾臻 1 '- '- '- 0 D13K48007@ntu.edu.tw;zaq4747@yahoo.com.tw
3 R13458003 林慕峰 1 '- '- '- 0 R13458003@ntu.edu.tw;brenton.lin.tw@gmail.com
4 R13458019 林 樸 1 '- '- '- 0 R13458019@ntu.edu.tw;pu@johnsonandannie.com
5 R13K47020 劉書瑜 1 '- '- '- 0 R13K47020@ntu.edu.tw;freestyle770614@gmail.com
6 R13K47029 陳品辰 1 '- '- '- 0 R13K47029@ntu.edu.tw;sw1sw2sw3@gmail.com
7 R13K47031 NANDHITHA SURULIANDI 1 '- '- '- 0 R13K47031@ntu.edu.tw;nandhithasuruliandi@gmail.com
8 D12K48020 傅冠豪 2 '- '- '- 0 D12K48020@ntu.edu.tw;haobbc@hotmail.com
9 R12458006 周宜葇 2 '- 通過 '- 0 R12458006@ntu.edu.tw;r12458006@g.ntu.edu.tw
10 R12458007 林育暄 2 '- '- '- 0 R12458007@ntu.edu.tw;sunny891228@gmail.com
11 R12458014 劉羽軒 2 '- '- '- 0 R12458014@ntu.edu.tw;liumilk613@gmail.com
12 R12458015 ZOLNAMAR DORJSEMBE 2 '- '- '- 0 R12458015@ntu.edu.tw;zolnamar@gmail.com
13 R11458015 郭士榮 3 '- '- '- 0 R11458015@ntu.edu.tw;tonypaul57@gmail.com

View file

View file

@ -0,0 +1,26 @@
from django.contrib import admin
from .models import Student, PointRecord
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
list_display = ('student_id', 'name', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'email', 'is_active')
search_fields = ('student_id', 'name')
ordering = ['student_id'] # Changed to order by student_id as an example
list_filter = ['grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'is_active']
@admin.register(PointRecord)
class PointRecordAdmin(admin.ModelAdmin):
list_display = ('student', 'datetime') # Updated to use datetime
search_fields = ['student__name', 'student__student_id']
list_filter = ['datetime'] # Updated to use datetime
ordering = ['-datetime'] # Updated to use datetime
fieldsets = [
(None, {
'fields': ['student']
}),
('點名時間', {
'fields': ['datetime'], # Updated to use datetime
'classes': ['collapse'] # 顯示為可展開的區塊
}),
]

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class StudentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'students'

View file

@ -0,0 +1,8 @@
from django import forms
class StudentLoginForm(forms.Form):
student_id_or_name = forms.CharField(
label='學號或姓名',
max_length=100,
required=True
)

View file

@ -0,0 +1,51 @@
import csv
from django.core.management.base import BaseCommand, CommandError
from ...models import Student
class Command(BaseCommand):
help = 'Import students from a CSV file'
def add_arguments(self, parser):
parser.add_argument('csv_file', type=str, help='The path to the CSV file')
def handle(self, *args, **kwargs):
csv_file_path = kwargs['csv_file']
with open(csv_file_path, newline='', encoding='utf-8-sig') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
student_id = row.get('學號\nStudent\nID')
name = row.get('姓名\nName')
if not student_id or not name:
self.stdout.write(self.style.WARNING(f"Skipping row due to missing student_id or name: {row}"))
continue
grade = int(row.get('年級\nyear', 0)) if row.get('年級\nyear') else None
hope_admission_scheme = row.get('希望生\nHope admission\nscheme students').strip("'") == '通過'
tuition_waiver = row.get('減免\nTuition\nwaiver').strip("'") == '通過'
financial_assistance_disadvantaged_students = row.get('弱勢_生輔組\nFinancial assistances\nfor disadvantaged\nstudents').strip("'") == '通過'
loan = int(row.get('就學貸款\nLoan', 0)) if row.get('就學貸款\nLoan') else 0
emails = row.get('Email').split(';')
# Use the first email for simplicity; you can handle multiple emails as needed
email = emails[0] if emails else None
student, created = Student.objects.update_or_create(
student_id=student_id,
defaults={
'name': name,
'grade': grade,
'hope_admission_scheme': hope_admission_scheme,
'tuition_waiver': tuition_waiver,
'financial_assistance_disadvantaged_students': financial_assistance_disadvantaged_students,
'loan': loan,
'email': email
}
)
if created:
self.stdout.write(self.style.SUCCESS(f"Successfully added student: {student}"))
else:
self.stdout.write(self.style.WARNING(f"Updated existing student: {student}"))

View file

@ -0,0 +1,41 @@
# Generated by Django 5.2 on 2025-07-25 02:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Student',
fields=[
('student_id', models.CharField(max_length=10, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('grade', models.IntegerField(blank=True, null=True)),
('hope_admission_scheme', models.BooleanField(blank=True, default=False, null=True)),
('tuition_waiver', models.BooleanField(blank=True, default=False, null=True)),
('financial_assistance_disadvantaged_students', models.BooleanField(blank=True, default=False, null=True)),
('loan', models.IntegerField(blank=True, default=0, null=True)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('is_active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='PointRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='students.student')),
],
options={
'verbose_name': 'point record',
'verbose_name_plural': 'point records',
},
),
]

View file

@ -0,0 +1,26 @@
from django.db import models
class Student(models.Model):
student_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=255)
grade = models.IntegerField(null=True, blank=True)
hope_admission_scheme = models.BooleanField(default=False, null=True, blank=True)
tuition_waiver = models.BooleanField(default=False, null=True, blank=True)
financial_assistance_disadvantaged_students = models.BooleanField(default=False, null=True, blank=True)
loan = models.IntegerField(null=True, blank=True, default=0)
email = models.EmailField(null=True, blank=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
class PointRecord(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
datetime = models.DateTimeField(auto_now_add=True) # Combined date and time field
class Meta:
verbose_name = 'point record'
verbose_name_plural = 'point records'
def __str__(self):
return f"{self.datetime} - {self.student}"

View file

@ -0,0 +1,71 @@
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}學生考勤系統{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{% url 'index' %}">
<i class="fas fa-user-graduate"></i> 學生考勤系統
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'index' %}">
<i class="fas fa-home"></i> 首頁
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'record_attendance' %}">
<i class="fas fa-clipboard-list"></i> 記錄考勤
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'recent_attendance' %}">
<i class="fas fa-history"></i> 最近考勤
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'attendance_report' %}">
<i class="fas fa-chart-bar"></i> 考勤報告
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'student_list' %}">
<i class="fas fa-users"></i> 學生列表
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主要內容 -->
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
<!-- 頁腳 -->
<footer class="bg-light text-center text-lg-start mt-5">
<div class="container p-4">
<p class="text-center">© 2023 學生考勤系統. All rights reserved.</p>
</div>
</footer>
<!-- JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<h1>年度出席統計報表</h1>
<table>
<tr>
<th>學號</th>
<th>姓名</th>
<th>{{ last_year }} 出席次數</th>
<th>{{ current_year }} 出席次數</th>
</tr>
{% for entry in attendance_data %}
<tr>
<td>{{ entry.student.student_id }}</td>
<td>{{ entry.student.name }}</td>
<td>{{ entry.last_year_count }}</td>
<td>{{ entry.current_year_count }}</td>
</tr>
{% endfor %}
</table>
<a href="{% url 'index' %}">返回首頁</a>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% block title %}最近考勤紀錄{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>最近20筆考勤紀錄</h2>
{% if recent_records %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>學生姓名</th>
<th>學號</th>
<th>考勤時間</th>
</tr>
</thead>
<tbody>
{% for record in recent_records %}
<tr>
<td>{{ record.student.name }}</td>
<td>{{ record.student.student_id }}</td>
<td>{{ record.datetime|date:"Y-m-d H:i:s" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p>目前沒有考勤紀錄。</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load tz %} <!-- Load the timezone template tags -->
{% block content %}
<h1>紀錄點名</h1>
<form method="post">
{% csrf_token %}
<label for="students">選擇學生:</label>
<div class="student-checklist">
{% for student in students %}
<label>
<input type="checkbox" name="student_id" value="{{ student.student_id }}">
{{ student.name }} ({{ student.student_id }})
</label><br>
{% endfor %}
</div>
<button type="submit">點名</button>
</form>
{% if point_records %}
<h2>Recent Attendance Records</h2>
<ul>
{% for record in point_records %}
<li>{{ record.datetime|timezone:"Asia/Taipei"|date:"Y-m-d H:i:s" }} - {{ record.student.name }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,36 @@
<!-- 在 templates/students/student_list.html -->
{% extends 'base.html' %}
{% block title %}學生列表{% endblock %}
{% block content %}
<h1>學生列表</h1>
<table class="table table-striped">
<thead>
<tr>
<th>學號</th>
<th>姓名</th>
<th>班級</th>
<th>考勤次數</th>
<th>詳細資料</th>
</tr>
</thead>
<tbody>
{% for item in student_data %}
<tr>
<td>{{ item.student.student_id }}</td>
<td>{{ item.student.name }}</td>
<td>{{ item.student.grade|default:"未填" }}</td>
<td>{{ item.total_attendance }}</td>
<td><a href="{% url 'show_attendance' item.student.student_id %}" class="btn btn-sm btn-primary">查看詳情</a></td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">沒有學生資料</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('record/', views.record_attendance, name='record_attendance'),
path('attendance/<str:student_id>/', views.show_attendance, name='show_attendance'),
path('report/', views.attendance_report, name='attendance_report'), # 新增路由
path('recent/', views.recent_attendance, name='recent_attendance'), # 新增最近考勤紀錄路由
path('student_list/', views.student_list, name='student_list'),
]

View file

@ -0,0 +1,84 @@
from django.shortcuts import render, redirect
from .models import Student, PointRecord # 從 students.models 引入 PointRecord
from .forms import StudentLoginForm
from datetime import datetime, timedelta
def index(request):
return render(request, 'base.html')
def record_attendance(request):
point_records = []
if request.method == 'POST':
student_ids = request.POST.getlist('student_id')
current_datetime = datetime.now()
one_hour_ago = current_datetime - timedelta(hours=1)
for student_id in student_ids:
try:
student = Student.objects.get(student_id=student_id)
# 檢查該學生在一小時內是否有點名紀錄
if not PointRecord.objects.filter(student=student, datetime__gte=one_hour_ago).exists():
point_record = PointRecord.objects.create(student=student, datetime=current_datetime)
point_records.append(point_record)
except Student.DoesNotExist:
pass
students = Student.objects.filter(is_active=True).order_by('name') # Filter active students
return render(request, 'students/record_attendance.html', {'students': students, 'point_records': point_records})
def show_attendance(request, student_id):
student = Student.objects.get(student_id=student_id)
point_records = PointRecord.objects.filter(student=student).order_by('-datetime')
return render(request, 'students/show_attendance.html', {'point_records': point_records, 'student': student})
def attendance_report(request):
current_year = datetime.now().year
last_year = current_year - 1
# 計算每個學生在去年和今年的出席次數
students = Student.objects.all()
attendance_data = []
for student in students:
last_year_records = PointRecord.objects.filter(student=student, datetime__year=last_year).count()
current_year_records = PointRecord.objects.filter(student=student, datetime__year=current_year).count()
# 只添加那些至少有一年的出席記錄的學生
if last_year_records > 0 or current_year_records > 0:
attendance_data.append({
'student': student,
'last_year_count': last_year_records,
'current_year_count': current_year_records
})
return render(request, 'students/attendance_report.html', {
'attendance_data': attendance_data,
'last_year': last_year,
'current_year': current_year
})
def recent_attendance(request):
"""顯示最後20筆考勤紀錄"""
recent_records = PointRecord.objects.select_related('student').order_by('-datetime')[:20]
return render(request, 'students/recent_attendance.html', {'recent_records': recent_records})
def student_list(request):
"""顯示所有學生資訊及考勤統計"""
students = Student.objects.all().order_by('student_id') # 按學號排序
# 為每個學生計算考勤統計
student_data = []
for student in students:
total_attendance = PointRecord.objects.filter(student=student).count()
# 可以添加更多統計資訊
student_data.append({
'student': student,
'total_attendance': total_attendance
})
return render(request, 'students/student_list.html', {
'student_data': student_data
})