feat(students): enhance student management and attendance reporting
Implement several improvements to the student management system, including: - Add `foreign_alias` and `admission_date` fields to the `Student` model. - Implement automatic `admission_date` calculation based on `student_id` patterns. - Enhance attendance reporting with internationalization (i18n) support and improved UI using Bootstrap components. - Update Excel import logic to support `foreign_alias` and `admission_date` columns. - Refine weekly schedule synchronization to respect student admission dates. - Improve the attendance recording interface with better UX and selection controls. - Update admin interface and templates to display new student attributes. - Add comprehensive test cases for authentication and protected view access.
This commit is contained in:
parent
c25a3afc59
commit
750b231a54
25 changed files with 872 additions and 292 deletions
1
point_system/formats/__init__.py
Normal file
1
point_system/formats/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Package initialization for formats
|
||||||
1
point_system/formats/en/__init__.py
Normal file
1
point_system/formats/en/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Package initialization for formats.en
|
||||||
19
point_system/formats/en/formats.py
Normal file
19
point_system/formats/en/formats.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.conf.locale.en.formats import *
|
||||||
|
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
'%Y-%m-%d',
|
||||||
|
'%Y/%m/%d',
|
||||||
|
'%Y.%m.%d',
|
||||||
|
'%Y年%m月%d日',
|
||||||
|
'%Y年%n月%j日',
|
||||||
|
'%m/%d/%Y',
|
||||||
|
'%m/%d/%y',
|
||||||
|
'%b %d %Y',
|
||||||
|
'%b %d, %Y',
|
||||||
|
'%d %b %Y',
|
||||||
|
'%d %b, %Y',
|
||||||
|
'%B %d %Y',
|
||||||
|
'%B %d, %Y',
|
||||||
|
'%d %B %Y',
|
||||||
|
'%d %B, %Y',
|
||||||
|
]
|
||||||
1
point_system/formats/zh_Hant/__init__.py
Normal file
1
point_system/formats/zh_Hant/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Package initialization for formats.zh_hant
|
||||||
1
point_system/formats/zh_Hant/formats.py
Normal file
1
point_system/formats/zh_Hant/formats.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from formats.zh_Hant.formats import *
|
||||||
|
|
@ -225,5 +225,80 @@ msgstr "%(count)s time"
|
||||||
msgid "無在學學生資料"
|
msgid "無在學學生資料"
|
||||||
msgstr "No active student data"
|
msgstr "No active student data"
|
||||||
|
|
||||||
|
msgid "在學學生: %(count)s 人"
|
||||||
|
msgstr "Active Students: %(count)s"
|
||||||
|
|
||||||
|
msgid "考勤紀錄提示:"
|
||||||
|
msgstr "Attendance Note: "
|
||||||
|
|
||||||
|
msgid "請勾選要紀錄考勤簽到的學生,點擊「確認考勤簽到」按鈕即可批次寫入點名紀錄。"
|
||||||
|
msgstr "Please select the students to record attendance for and click \"Submit Attendance\" to save."
|
||||||
|
|
||||||
|
msgid "學生考勤點名單"
|
||||||
|
msgstr "Student Attendance Checklist"
|
||||||
|
|
||||||
|
msgid "全選"
|
||||||
|
msgstr "Select All"
|
||||||
|
|
||||||
|
msgid "取消全選"
|
||||||
|
msgstr "Deselect All"
|
||||||
|
|
||||||
|
msgid "搜尋學生姓名..."
|
||||||
|
msgstr "Search student name..."
|
||||||
|
|
||||||
|
msgid "已勾選:"
|
||||||
|
msgstr "Selected: "
|
||||||
|
|
||||||
|
msgid "確認考勤簽到"
|
||||||
|
msgstr "Submit Attendance"
|
||||||
|
|
||||||
|
msgid "本次已成功紀錄考勤"
|
||||||
|
msgstr "Attendance Recorded Successfully"
|
||||||
|
|
||||||
|
msgid "共 %(count)s 筆紀錄"
|
||||||
|
msgstr "%(count)s Record(s)"
|
||||||
|
|
||||||
|
msgid "身分 / 學位"
|
||||||
|
msgstr "Category / Degree"
|
||||||
|
|
||||||
|
msgid "考勤簽到時間"
|
||||||
|
msgstr "Attendance Timestamp"
|
||||||
|
|
||||||
|
msgid "已簽到"
|
||||||
|
msgstr "Recorded"
|
||||||
|
|
||||||
|
msgid "最新紀錄: %(count)s 筆"
|
||||||
|
msgstr "Latest Records: %(count)s"
|
||||||
|
|
||||||
|
msgid "系統狀態: 即時記錄"
|
||||||
|
msgstr "Status: Real-time"
|
||||||
|
|
||||||
|
msgid "顯示系統中最新輸入之 20 筆學生考勤簽到時間明細,點擊學生姓名或查看詳情可瀏覽個人完整簽到與報告紀錄。"
|
||||||
|
msgstr "Displays the 20 most recent student attendance records. Click student name or view details to see full history."
|
||||||
|
|
||||||
|
msgid "最近 20 筆考勤簽到紀錄"
|
||||||
|
msgstr "Recent 20 Attendance Records"
|
||||||
|
|
||||||
|
msgid "目前沒有考勤紀錄。"
|
||||||
|
msgstr "No attendance records found."
|
||||||
|
|
||||||
|
msgid "統計學生數: %(count)s 人"
|
||||||
|
msgstr "Students Count: %(count)s"
|
||||||
|
|
||||||
|
msgid "報表統計提示:"
|
||||||
|
msgstr "Report Summary: "
|
||||||
|
|
||||||
|
msgid "統計各學生於去年度 (%(ly)s) 與本年度 (%(cy)s) 之累積考勤簽到次數總和。"
|
||||||
|
msgstr "Annual cumulative attendance counts for last year (%(ly)s) and current year (%(cy)s)."
|
||||||
|
|
||||||
|
msgid "年度出席統計報表 (%(ly)s vs %(cy)s)"
|
||||||
|
msgstr "Annual Attendance Report (%(ly)s vs %(cy)s)"
|
||||||
|
|
||||||
|
msgid "出席次數"
|
||||||
|
msgstr "Attendance Count"
|
||||||
|
|
||||||
|
msgid "尚無符合條件之考勤統計資料"
|
||||||
|
msgstr "No attendance statistics available."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,21 @@ LOCALE_PATHS = [
|
||||||
BASE_DIR / 'locale',
|
BASE_DIR / 'locale',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
FORMAT_MODULE_PATH = [
|
||||||
|
'formats',
|
||||||
|
]
|
||||||
|
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
'%Y-%m-%d',
|
||||||
|
'%Y/%m/%d',
|
||||||
|
'%Y.%m.%d',
|
||||||
|
'%Y年%m月%d日',
|
||||||
|
'%Y年%n月%j日',
|
||||||
|
'%m/%d/%Y',
|
||||||
|
'%m/%d/%y',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-08-10 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-08-10 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-primary fs-6">6 presenters</span>
|
<span class="badge bg-light text-primary fs-6">4 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -92,22 +92,6 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">BUSHRA UROOJ</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
|
||||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林 樸</td>
|
<td class="fw-bold">林 樸</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -125,7 +109,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">李亮慶</td>
|
<td class="fw-bold">Frank</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -141,23 +125,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">Emran Khan</td>
|
<td class="fw-bold">Eric</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">林宇晨</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-primary text-white"><i
|
<span class="badge bg-primary text-white"><i
|
||||||
|
|
@ -204,7 +172,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-08-17 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-08-17 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-primary fs-6">6 presenters</span>
|
<span class="badge bg-light text-primary fs-6">4 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -220,7 +188,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林孟忻</td>
|
<td class="fw-bold">Gloria</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -236,23 +204,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉冠岑</td>
|
<td class="fw-bold">Sandy</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">江霈</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -299,22 +251,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">孫艾琳</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-warning text-dark"><i
|
|
||||||
class="fas fa-user-tie me-1"></i>In-Service</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -378,7 +314,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉睿恩</td>
|
<td class="fw-bold">Roy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -473,7 +409,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林靖哲</td>
|
<td class="fw-bold">Jimmy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -489,7 +425,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林綺音</td>
|
<td class="fw-bold">Yin</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -579,7 +515,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林孟忻</td>
|
<td class="fw-bold">Gloria</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -611,7 +547,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉冠岑</td>
|
<td class="fw-bold">Sandy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -627,7 +563,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林宇晨</td>
|
<td class="fw-bold">Eric</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-primary text-white"><i
|
<span class="badge bg-primary text-white"><i
|
||||||
|
|
@ -644,7 +580,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">陳其蒙</td>
|
<td class="fw-bold">Kimo</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -690,7 +626,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-09-14 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-09-14 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-success fs-6">6 presenters</span>
|
<span class="badge bg-light text-success fs-6">5 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -706,23 +642,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">BUSHRA UROOJ</td>
|
<td class="fw-bold">Roy</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
|
||||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">劉睿恩</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -754,7 +674,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林綺音</td>
|
<td class="fw-bold">Yin</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -848,7 +768,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林靖哲</td>
|
<td class="fw-bold">Jimmy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -864,7 +784,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">江霈</td>
|
<td class="fw-bold">Pei</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -959,7 +879,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">李亮慶</td>
|
<td class="fw-bold">Frank</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -992,7 +912,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">黃子芬</td>
|
<td class="fw-bold">Melissa</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,35 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django import forms
|
||||||
from .models import Student, PointRecord, WeeklySchedule
|
from .models import Student, PointRecord, WeeklySchedule
|
||||||
|
|
||||||
|
|
||||||
|
class StudentForm(forms.ModelForm):
|
||||||
|
admission_date = forms.DateField(
|
||||||
|
label='入學日期',
|
||||||
|
required=False,
|
||||||
|
input_formats=[
|
||||||
|
'%Y-%m-%d',
|
||||||
|
'%Y/%m/%d',
|
||||||
|
'%Y.%m.%d',
|
||||||
|
'%Y年%m月%d日',
|
||||||
|
'%Y年%n月%j日',
|
||||||
|
'%m/%d/%Y',
|
||||||
|
'%m/%d/%y',
|
||||||
|
],
|
||||||
|
widget=admin.widgets.AdminDateWidget()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Student
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
@admin.register(Student)
|
@admin.register(Student)
|
||||||
class StudentAdmin(admin.ModelAdmin):
|
class StudentAdmin(admin.ModelAdmin):
|
||||||
list_display = ('student_id', 'name', 'degree_type', 'student_category', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'email', 'email2', 'is_active')
|
form = StudentForm
|
||||||
search_fields = ('student_id', 'name', 'email', 'email2')
|
list_display = ('student_id', 'name', 'foreign_alias', 'degree_type', 'student_category', 'grade', 'admission_date', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'email', 'email2', 'is_active')
|
||||||
|
search_fields = ('student_id', 'name', 'foreign_alias', 'email', 'email2')
|
||||||
ordering = ['student_id']
|
ordering = ['student_id']
|
||||||
list_filter = ['degree_type', 'student_category', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'is_active']
|
list_filter = ['degree_type', 'student_category', 'grade', 'admission_date', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'is_active']
|
||||||
|
|
||||||
@admin.register(PointRecord)
|
@admin.register(PointRecord)
|
||||||
class PointRecordAdmin(admin.ModelAdmin):
|
class PointRecordAdmin(admin.ModelAdmin):
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class ADLoginForm(forms.Form):
|
||||||
label='AD 帳號',
|
label='AD 帳號',
|
||||||
max_length=150,
|
max_length=150,
|
||||||
required=True,
|
required=True,
|
||||||
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '帳號 (例如: user 或 user@samdom.mdi.bar)'})
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '帳號 (例如: user 或 user@domain)'})
|
||||||
)
|
)
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label='密碼',
|
label='密碼',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2 on 2026-08-05 04:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('students', '0007_alter_student_degree_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='student',
|
||||||
|
name='foreign_alias',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='外文別名'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import re
|
||||||
|
from datetime import date
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_admission_dates(apps, schema_editor):
|
||||||
|
Student = apps.get_model('students', 'Student')
|
||||||
|
for student in Student.objects.all():
|
||||||
|
if not student.admission_date and student.student_id:
|
||||||
|
sid = str(student.student_id).strip()
|
||||||
|
m_letter = re.match(r'^[A-Za-z]+(\d{2})', sid)
|
||||||
|
if m_letter:
|
||||||
|
yy = int(m_letter.group(1))
|
||||||
|
roc_year = (100 + yy) if yy < 90 else yy
|
||||||
|
ad_year = roc_year + 1911
|
||||||
|
student.admission_date = date(ad_year, 9, 1)
|
||||||
|
else:
|
||||||
|
m_roc3 = re.match(r'^(\d{3})', sid)
|
||||||
|
if m_roc3:
|
||||||
|
roc_year = int(m_roc3.group(1))
|
||||||
|
ad_year = roc_year + 1911
|
||||||
|
student.admission_date = date(ad_year, 9, 1)
|
||||||
|
else:
|
||||||
|
student.admission_date = date(date.today().year, 9, 1)
|
||||||
|
student.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('students', '0008_student_foreign_alias'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='student',
|
||||||
|
name='admission_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='入學日期'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(populate_admission_dates, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
from datetime import date
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
DEGREE_CHOICES = [
|
DEGREE_CHOICES = [
|
||||||
|
|
@ -12,12 +14,35 @@ CATEGORY_CHOICES = [
|
||||||
('交換中', '交換中'),
|
('交換中', '交換中'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def calculate_admission_date(student_id):
|
||||||
|
if not student_id:
|
||||||
|
return date(date.today().year, 9, 1)
|
||||||
|
|
||||||
|
sid = str(student_id).strip()
|
||||||
|
|
||||||
|
m_letter = re.match(r'^[A-Za-z]+(\d{2})', sid)
|
||||||
|
if m_letter:
|
||||||
|
yy = int(m_letter.group(1))
|
||||||
|
roc_year = (100 + yy) if yy < 90 else yy
|
||||||
|
ad_year = roc_year + 1911
|
||||||
|
return date(ad_year, 9, 1)
|
||||||
|
|
||||||
|
m_roc3 = re.match(r'^(\d{3})', sid)
|
||||||
|
if m_roc3:
|
||||||
|
roc_year = int(m_roc3.group(1))
|
||||||
|
ad_year = roc_year + 1911
|
||||||
|
return date(ad_year, 9, 1)
|
||||||
|
|
||||||
|
return date(date.today().year, 9, 1)
|
||||||
|
|
||||||
class Student(models.Model):
|
class Student(models.Model):
|
||||||
student_id = models.CharField(max_length=10, primary_key=True)
|
student_id = models.CharField(max_length=10, primary_key=True)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
foreign_alias = models.CharField(max_length=255, null=True, blank=True, verbose_name='外文別名')
|
||||||
degree_type = models.CharField(max_length=10, choices=DEGREE_CHOICES, default='碩士生', verbose_name='學位類別')
|
degree_type = models.CharField(max_length=10, choices=DEGREE_CHOICES, default='碩士生', verbose_name='學位類別')
|
||||||
student_category = models.CharField(max_length=10, choices=CATEGORY_CHOICES, default='一般生', verbose_name='身分類別')
|
student_category = models.CharField(max_length=10, choices=CATEGORY_CHOICES, default='一般生', verbose_name='身分類別')
|
||||||
grade = models.IntegerField(null=True, blank=True)
|
grade = models.IntegerField(null=True, blank=True)
|
||||||
|
admission_date = models.DateField(null=True, blank=True, verbose_name='入學日期')
|
||||||
hope_admission_scheme = models.BooleanField(default=False, 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)
|
tuition_waiver = models.BooleanField(default=False, null=True, blank=True)
|
||||||
financial_assistance_disadvantaged_students = models.BooleanField(default=False, null=True, blank=True)
|
financial_assistance_disadvantaged_students = models.BooleanField(default=False, null=True, blank=True)
|
||||||
|
|
@ -26,6 +51,11 @@ class Student(models.Model):
|
||||||
email2 = models.EmailField(null=True, blank=True, verbose_name='第二 Email')
|
email2 = models.EmailField(null=True, blank=True, verbose_name='第二 Email')
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.admission_date and self.student_id:
|
||||||
|
self.admission_date = calculate_admission_date(self.student_id)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.degree_type} / {self.student_category})"
|
return f"{self.name} ({self.degree_type} / {self.student_category})"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-08-10 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-08-10 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-primary fs-6">6 presenters</span>
|
<span class="badge bg-light text-primary fs-6">4 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -92,22 +92,6 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">BUSHRA UROOJ</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
|
||||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林 樸</td>
|
<td class="fw-bold">林 樸</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -125,7 +109,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">李亮慶</td>
|
<td class="fw-bold">Frank</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -141,23 +125,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">Emran Khan</td>
|
<td class="fw-bold">Eric</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">林宇晨</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-primary text-white"><i
|
<span class="badge bg-primary text-white"><i
|
||||||
|
|
@ -204,7 +172,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-08-17 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-08-17 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-primary fs-6">6 presenters</span>
|
<span class="badge bg-light text-primary fs-6">4 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -220,7 +188,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林孟忻</td>
|
<td class="fw-bold">Gloria</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -236,23 +204,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉冠岑</td>
|
<td class="fw-bold">Sandy</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">江霈</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -299,22 +251,6 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">孫艾琳</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
|
||||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-warning text-dark"><i
|
|
||||||
class="fas fa-user-tie me-1"></i>In-Service</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -378,7 +314,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉睿恩</td>
|
<td class="fw-bold">Roy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -473,7 +409,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林靖哲</td>
|
<td class="fw-bold">Jimmy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -489,7 +425,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林綺音</td>
|
<td class="fw-bold">Yin</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -579,7 +515,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林孟忻</td>
|
<td class="fw-bold">Gloria</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -611,7 +547,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">劉冠岑</td>
|
<td class="fw-bold">Sandy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -627,7 +563,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林宇晨</td>
|
<td class="fw-bold">Eric</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-primary text-white"><i
|
<span class="badge bg-primary text-white"><i
|
||||||
|
|
@ -644,7 +580,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">陳其蒙</td>
|
<td class="fw-bold">Kimo</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -690,7 +626,7 @@
|
||||||
<h5 class="mb-0 fw-bold">
|
<h5 class="mb-0 fw-bold">
|
||||||
<i class="fas fa-calendar-check me-2"></i>2026-09-14 (Monday)
|
<i class="fas fa-calendar-check me-2"></i>2026-09-14 (Monday)
|
||||||
</h5>
|
</h5>
|
||||||
<span class="badge bg-light text-success fs-6">6 presenters</span>
|
<span class="badge bg-light text-success fs-6">5 presenters</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
|
|
@ -706,23 +642,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">BUSHRA UROOJ</td>
|
<td class="fw-bold">Roy</td>
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
|
||||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<span class="badge bg-success text-white"><i
|
|
||||||
class="fas fa-user me-1"></i>Regular</span>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">劉睿恩</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -754,7 +674,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林綺音</td>
|
<td class="fw-bold">Yin</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -848,7 +768,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">林靖哲</td>
|
<td class="fw-bold">Jimmy</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -864,7 +784,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">江霈</td>
|
<td class="fw-bold">Pei</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -959,7 +879,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">李亮慶</td>
|
<td class="fw-bold">Frank</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-info text-dark"><i
|
<span class="badge bg-info text-dark"><i
|
||||||
|
|
@ -992,7 +912,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">黃子芬</td>
|
<td class="fw-bold">Melissa</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,7 @@
|
||||||
<footer class="bg-light text-center text-lg-start mt-5 py-4 border-top">
|
<footer class="bg-light text-center text-lg-start mt-5 py-4 border-top">
|
||||||
<div class="container text-center text-muted small">
|
<div class="container text-center text-muted small">
|
||||||
<p class="mb-1">© 2023-2026 1218 Lab. All rights reserved.</p>
|
<p class="mb-1">© 2023-2026 1218 Lab. All rights reserved.</p>
|
||||||
<p class="mb-0">Active Directory DC: <code>dc1.samdom.mdi.bar</code> (Domain: <code>samdom.mdi.bar</code>)
|
<p class="mb-0"><i class="fas fa-shield-alt me-1 text-primary"></i> Active Directory Single Sign-On</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,93 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "考勤報告" %} - 1218 Lab{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>年度出席統計報表</h1>
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
|
||||||
|
<h1 class="h2 text-primary mb-0">
|
||||||
|
<i class="fas fa-chart-bar me-2"></i>{% trans "考勤報告" %}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-success fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i>{{ last_year }} - {{ current_year }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-users me-1"></i>{% blocktrans with count=attendance_data|length %}統計學生數: {{ count }} 人{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table>
|
<div class="alert alert-info small mb-4 shadow-sm rounded-3 border-start border-4 border-info">
|
||||||
<tr>
|
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||||
<th>學號</th>
|
<strong>{% trans "報表統計提示:" %}</strong>{% blocktrans with ly=last_year cy=current_year %}統計各學生於去年度 ({{ ly }}) 與本年度 ({{ cy }}) 之累積考勤簽到次數總和。{% endblocktrans %}
|
||||||
<th>姓名</th>
|
</div>
|
||||||
<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>
|
<div class="card shadow-sm border-0 rounded-3 mb-4">
|
||||||
|
<div class="card-header bg-primary text-white py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0 fw-bold">
|
||||||
|
<i class="fas fa-table me-2"></i>{% blocktrans with ly=last_year cy=current_year %}年度出席統計報表 ({{ ly }} vs {{ cy }}){% endblocktrans %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if attendance_data %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" style="width: 8%;">#</th>
|
||||||
|
<th scope="col" style="width: 27%;">{% trans "姓名" %}</th>
|
||||||
|
<th scope="col" style="width: 25%;">{% trans "身分 / 學位" %}</th>
|
||||||
|
<th scope="col" style="width: 20%;">{{ last_year }} {% trans "出席次數" %}</th>
|
||||||
|
<th scope="col" style="width: 20%;">{{ current_year }} {% trans "出席次數" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in attendance_data %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold text-muted">{{ forloop.counter }}</td>
|
||||||
|
<td class="fw-bold">
|
||||||
|
{% get_current_language as CURRENT_LANG %}
|
||||||
|
{% if CURRENT_LANG == 'en' and entry.student.foreign_alias %}
|
||||||
|
{{ entry.student.foreign_alias }}
|
||||||
|
{% else %}
|
||||||
|
{{ entry.student.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.student.degree_type == '博士生' %}
|
||||||
|
<span class="badge bg-danger text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||||
|
{% elif entry.student.degree_type == '學士生' %}
|
||||||
|
<span class="badge bg-primary text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "學士生" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info text-dark"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.student.student_category == '在職生' %}
|
||||||
|
<span class="badge bg-warning text-dark"><i class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||||
|
{% elif entry.student.student_category == '交換中' %}
|
||||||
|
<span class="badge bg-secondary text-white"><i class="fas fa-exchange-alt me-1"></i>{% trans "交換中" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success text-white"><i class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary fs-6">{{ entry.last_year_count }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary fs-6">{{ entry.current_year_count }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<i class="fas fa-folder-open fa-2x mb-2 d-block"></i>
|
||||||
|
{% trans "尚無符合條件之考勤統計資料" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
<div class="card shadow-sm border-0 rounded-3">
|
<div class="card shadow-sm border-0 rounded-3">
|
||||||
<div class="card-header bg-primary text-white text-center py-3 rounded-top">
|
<div class="card-header bg-primary text-white text-center py-3 rounded-top">
|
||||||
<h4 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Active Directory 登入</h4>
|
<h4 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Active Directory 登入</h4>
|
||||||
<small class="text-white-50">DC: dc1.samdom.mdi.bar</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
|
@ -27,7 +26,7 @@
|
||||||
<i class="fas fa-user me-1 text-primary"></i> AD 帳號
|
<i class="fas fa-user me-1 text-primary"></i> AD 帳號
|
||||||
</label>
|
</label>
|
||||||
{{ form.username }}
|
{{ form.username }}
|
||||||
<div class="form-text">可以使用短帳號 (例如: <code>username</code>) 或完整 UPN (例如: <code>username@samdom.mdi.bar</code>)。</div>
|
<div class="form-text">可以使用短帳號 (例如: <code>username</code>) 或完整 UPN (例如: <code>username@domain</code>)。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="{{ form.password.id_for_label }}" class="form-label fw-bold">
|
<label for="{{ form.password.id_for_label }}" class="form-label fw-bold">
|
||||||
|
|
@ -43,7 +42,7 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center bg-light py-3 text-muted small">
|
<div class="card-footer text-center bg-light py-3 text-muted small">
|
||||||
網域: <code>samdom.mdi.bar</code> | Domain Controller: <code>dc1.samdom.mdi.bar</code>
|
<i class="fas fa-shield-alt me-1 text-primary"></i> Active Directory Single Sign-On
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,89 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n tz %}
|
||||||
|
|
||||||
{% block title %}最近考勤紀錄{% endblock %}
|
{% block title %}{% trans "最近考勤" %} - 1218 Lab{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-4">
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
|
||||||
<h2>最近20筆考勤紀錄</h2>
|
<h1 class="h2 text-primary mb-0">
|
||||||
|
<i class="fas fa-history me-2"></i>{% trans "最近考勤" %}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-success fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-list-ol me-1"></i>{% blocktrans with count=recent_records|length %}最新紀錄: {{ count }} 筆{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-clock me-1"></i>{% trans "系統狀態: 即時記錄" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if recent_records %}
|
<div class="alert alert-info small mb-4 shadow-sm rounded-3 border-start border-4 border-info">
|
||||||
|
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||||
|
<strong>{% trans "考勤紀錄提示:" %}</strong>{% trans "顯示系統中最新輸入之 20 筆學生考勤簽到時間明細,點擊學生姓名或查看詳情可瀏覽個人完整簽到與報告紀錄。" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 rounded-3 mb-4">
|
||||||
|
<div class="card-header bg-primary text-white py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0 fw-bold">
|
||||||
|
<i class="fas fa-list-alt me-2"></i>{% trans "最近 20 筆考勤簽到紀錄" %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if recent_records %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>學生姓名</th>
|
<th scope="col" style="width: 10%;">#</th>
|
||||||
<th>學號</th>
|
<th scope="col" style="width: 30%;">{% trans "姓名" %}</th>
|
||||||
<th>考勤時間</th>
|
<th scope="col" style="width: 30%;">{% trans "身分 / 學位" %}</th>
|
||||||
|
<th scope="col" style="width: 30%;">{% trans "考勤簽到時間" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for record in recent_records %}
|
{% for record in recent_records %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ record.student.name }}</td>
|
<td class="fw-bold text-muted">{{ forloop.counter }}</td>
|
||||||
<td>{{ record.student.student_id }}</td>
|
<td class="fw-bold">
|
||||||
<td>{{ record.datetime|date:"Y-m-d H:i:s" }}</td>
|
{% get_current_language as CURRENT_LANG %}
|
||||||
|
{% if CURRENT_LANG == 'en' and record.student.foreign_alias %}
|
||||||
|
{{ record.student.foreign_alias }}
|
||||||
|
{% else %}
|
||||||
|
{{ record.student.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if record.student.degree_type == '博士生' %}
|
||||||
|
<span class="badge bg-danger text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||||
|
{% elif record.student.degree_type == '學士生' %}
|
||||||
|
<span class="badge bg-primary text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "學士生" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info text-dark"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if record.student.student_category == '在職生' %}
|
||||||
|
<span class="badge bg-warning text-dark"><i class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||||
|
{% elif record.student.student_category == '交換中' %}
|
||||||
|
<span class="badge bg-secondary text-white"><i class="fas fa-exchange-alt me-1"></i>{% trans "交換中" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success text-white"><i class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-primary fw-bold">
|
||||||
|
<i class="far fa-clock me-1"></i>{{ record.datetime|timezone:"Asia/Taipei"|date:"Y-m-d H:i:s" }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>目前沒有考勤紀錄。</p>
|
<div class="text-center py-5 text-muted">
|
||||||
{% endif %}
|
<i class="fas fa-folder-open fa-2x mb-2 d-block"></i>
|
||||||
|
{% trans "目前沒有考勤紀錄。" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,30 +1,203 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load tz %} <!-- Load the timezone template tags -->
|
{% load i18n tz %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "記錄考勤" %} - 1218 Lab{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>紀錄點名</h1>
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-3">
|
||||||
|
<h1 class="h2 text-primary mb-0">
|
||||||
<form method="post">
|
<i class="fas fa-clipboard-check me-2"></i>{% trans "記錄考勤" %}
|
||||||
{% csrf_token %}
|
</h1>
|
||||||
<label for="students">選擇學生:</label>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div class="student-checklist">
|
<button type="button" class="btn btn-outline-primary btn-sm fw-bold shadow-sm me-1" onclick="toggleSelectAll(true)">
|
||||||
{% for student in students %}
|
<i class="fas fa-check-square me-1"></i>{% trans "全選" %}
|
||||||
<label>
|
</button>
|
||||||
<input type="checkbox" name="student_id" value="{{ student.student_id }}">
|
<button type="button" class="btn btn-outline-secondary btn-sm fw-bold shadow-sm me-2" onclick="toggleSelectAll(false)">
|
||||||
{{ student.name }} ({{ student.student_id }})
|
<i class="far fa-square me-1"></i>{% trans "取消全選" %}
|
||||||
</label><br>
|
</button>
|
||||||
{% endfor %}
|
<span class="badge bg-success fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-users me-1"></i>{% blocktrans with count=students|length %}在學學生: {{ count }} 人{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
|
||||||
|
<i class="fas fa-clock me-1"></i>{% now "Y-m-d H:i" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" id="attendanceForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="card shadow-sm border-0 rounded-3 mb-4">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover table-striped table-borderless align-middle mb-0">
|
||||||
|
<tbody id="studentCardGrid">
|
||||||
|
{% for student in students %}
|
||||||
|
<tr class="student-tile-col" data-student-name="{{ student.name|lower }}" data-student-alias="{{ student.foreign_alias|default:''|lower }}" style="cursor: pointer;" onclick="handleRowClick(event, 'chk_{{ student.student_id }}')">
|
||||||
|
<td class="ps-3 py-1 text-center" style="width: 45px;">
|
||||||
|
<input type="checkbox" name="student_id" value="{{ student.student_id }}" class="form-check-input student-checkbox mt-0" id="chk_{{ student.student_id }}" style="transform: scale(1.2); cursor: pointer;" onchange="updateRowStyle(this)">
|
||||||
|
</td>
|
||||||
|
<td class="py-1">
|
||||||
|
<label class="form-check-label text-dark mb-0 fw-bold" for="chk_{{ student.student_id }}" style="cursor: pointer; font-size: 18px;">
|
||||||
|
{% get_current_language as CURRENT_LANG %}
|
||||||
|
{% if CURRENT_LANG == 'en' and student.foreign_alias %}
|
||||||
|
{{ student.foreign_alias }}
|
||||||
|
{% else %}
|
||||||
|
{{ student.name }}
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class="py-1 text-end pe-3">
|
||||||
|
{% if student.degree_type == '博士生' %}
|
||||||
|
<span class="badge bg-danger text-white fs-6 py-1 px-2.5"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||||
|
{% elif student.degree_type == '學士生' %}
|
||||||
|
<span class="badge bg-primary text-white fs-6 py-1 px-2.5"><i class="fas fa-user-graduate me-1"></i>{% trans "學士生" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info text-dark fs-6 py-1 px-2.5"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if student.student_category == '在職生' %}
|
||||||
|
<span class="badge bg-warning text-dark fs-6 py-1 px-2.5"><i class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||||
|
{% elif student.student_category == '交換中' %}
|
||||||
|
<span class="badge bg-secondary text-white fs-6 py-1 px-2.5"><i class="fas fa-exchange-alt me-1"></i>{% trans "交換中" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success text-white fs-6 py-1 px-2.5"><i class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-4 text-muted">
|
||||||
|
<i class="fas fa-user-slash me-1"></i>{% trans "無在學學生資料" %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-light p-3 d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||||
|
<span class="text-muted fw-bold fs-6">
|
||||||
|
<i class="fas fa-user-check text-primary me-1"></i>{% trans "已勾選:" %} <span id="selectedCountBadge" class="badge bg-primary fs-6">0</span> 人
|
||||||
|
</span>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg shadow-sm fw-bold px-4">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>{% trans "確認考勤簽到" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">點名</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if point_records %}
|
{% if point_records %}
|
||||||
<h2>Recent Attendance Records</h2>
|
<div class="card shadow-sm border-0 rounded-3 mb-4 mt-5">
|
||||||
<ul>
|
<div class="card-header bg-success text-white py-3 d-flex justify-content-between align-items-center">
|
||||||
{% for record in point_records %}
|
<h5 class="mb-0 fw-bold">
|
||||||
<li>{{ record.datetime|timezone:"Asia/Taipei"|date:"Y-m-d H:i:s" }} - {{ record.student.name }}</li>
|
<i class="fas fa-check-double me-2"></i>{% trans "本次已成功紀錄考勤" %}
|
||||||
{% endfor %}
|
</h5>
|
||||||
</ul>
|
<span class="badge bg-light text-success fs-6">{% blocktrans with count=point_records|length %}共 {{ count }} 筆紀錄{% endblocktrans %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 10%;">#</th>
|
||||||
|
<th style="width: 30%;">{% trans "姓名" %}</th>
|
||||||
|
<th style="width: 30%;">{% trans "身分 / 學位" %}</th>
|
||||||
|
<th style="width: 30%;">{% trans "考勤簽到時間" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in point_records %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold text-muted">{{ forloop.counter }}</td>
|
||||||
|
<td class="fw-bold">
|
||||||
|
{% get_current_language as CURRENT_LANG %}
|
||||||
|
{% if CURRENT_LANG == 'en' and record.student.foreign_alias %}
|
||||||
|
{{ record.student.foreign_alias }}
|
||||||
|
{% else %}
|
||||||
|
{{ record.student.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if record.student.degree_type == '博士生' %}
|
||||||
|
<span class="badge bg-danger text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||||
|
{% elif record.student.degree_type == '學士生' %}
|
||||||
|
<span class="badge bg-primary text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "學士生" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-info text-dark"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if record.student.student_category == '在職生' %}
|
||||||
|
<span class="badge bg-warning text-dark"><i class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||||
|
{% elif record.student.student_category == '交換中' %}
|
||||||
|
<span class="badge bg-secondary text-white"><i class="fas fa-exchange-alt me-1"></i>{% trans "交換中" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success text-white"><i class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-primary fw-bold">
|
||||||
|
<i class="far fa-clock me-1"></i>{{ record.datetime|timezone:"Asia/Taipei"|date:"Y-m-d H:i:s" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function handleRowClick(event, chkId) {
|
||||||
|
if (event.target.tagName.toLowerCase() === 'input' || event.target.tagName.toLowerCase() === 'label') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chk = document.getElementById(chkId);
|
||||||
|
if (chk) {
|
||||||
|
chk.checked = !chk.checked;
|
||||||
|
updateRowStyle(chk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowStyle(chk) {
|
||||||
|
const tr = chk.closest('tr');
|
||||||
|
if (chk.checked) {
|
||||||
|
tr.classList.add('table-primary');
|
||||||
|
} else {
|
||||||
|
tr.classList.remove('table-primary');
|
||||||
|
}
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll(select) {
|
||||||
|
const checkboxes = document.querySelectorAll('.student-checkbox');
|
||||||
|
checkboxes.forEach(chk => {
|
||||||
|
const tr = chk.closest('.student-tile-col');
|
||||||
|
if (tr.style.display !== 'none') {
|
||||||
|
chk.checked = select;
|
||||||
|
updateRowStyle(chk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedCount() {
|
||||||
|
const count = document.querySelectorAll('.student-checkbox:checked').length;
|
||||||
|
const badge = document.getElementById('selectedCountBadge');
|
||||||
|
if (badge) badge.textContent = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStudents() {
|
||||||
|
const query = document.getElementById('studentSearchInput').value.toLowerCase().trim();
|
||||||
|
const rows = document.querySelectorAll('.student-tile-col');
|
||||||
|
rows.forEach(tr => {
|
||||||
|
const name = tr.getAttribute('data-student-name') || '';
|
||||||
|
const alias = tr.getAttribute('data-student-alias') || '';
|
||||||
|
if (name.includes(query) || alias.includes(query)) {
|
||||||
|
tr.style.display = '';
|
||||||
|
} else {
|
||||||
|
tr.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -18,20 +18,21 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-3 col-sm-6">
|
<div class="col-md-2 col-sm-6">
|
||||||
<div class="p-3 bg-light rounded border">
|
<div class="p-3 bg-light rounded border h-100">
|
||||||
<small class="text-muted d-block mb-1">學號</small>
|
<small class="text-muted d-block mb-1">學號</small>
|
||||||
<span class="h5 fw-bold text-dark"><code>{{ student.student_id }}</code></span>
|
<span class="h5 fw-bold text-dark"><code>{{ student.student_id }}</code></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-6">
|
<div class="col-md-3 col-sm-6">
|
||||||
<div class="p-3 bg-light rounded border">
|
<div class="p-3 bg-light rounded border h-100">
|
||||||
<small class="text-muted d-block mb-1">姓名</small>
|
<small class="text-muted d-block mb-1">姓名</small>
|
||||||
<span class="h5 fw-bold text-dark">{{ student.name }}</span>
|
<span class="h5 fw-bold text-dark">{{ student.name }}</span>
|
||||||
|
{% if student.foreign_alias %}<small class="text-muted d-block">({{ student.foreign_alias }})</small>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-6">
|
<div class="col-md-3 col-sm-6">
|
||||||
<div class="p-3 bg-light rounded border">
|
<div class="p-3 bg-light rounded border h-100">
|
||||||
<small class="text-muted d-block mb-1">學位 / 身分</small>
|
<small class="text-muted d-block mb-1">學位 / 身分</small>
|
||||||
<div>
|
<div>
|
||||||
{% if student.degree_type == '博士生' %}
|
{% if student.degree_type == '博士生' %}
|
||||||
|
|
@ -52,8 +53,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-6">
|
<div class="col-md-2 col-sm-6">
|
||||||
<div class="p-3 bg-light rounded border">
|
<div class="p-3 bg-light rounded border h-100">
|
||||||
|
<small class="text-muted d-block mb-1">入學日期</small>
|
||||||
|
<span class="h6 fw-bold text-dark"><i class="far fa-calendar-alt text-primary me-1"></i>{{ student.admission_date|date:"Y-m-d"|default:"--" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-6">
|
||||||
|
<div class="p-3 bg-light rounded border h-100">
|
||||||
<small class="text-muted d-block mb-1">總簽到次數</small>
|
<small class="text-muted d-block mb-1">總簽到次數</small>
|
||||||
<span class="h5 fw-bold text-primary">{% if can_view_records %}{{ point_records|length }} 次{% else %}<i class="fas fa-lock me-1"></i>受限{% endif %}</span>
|
<span class="h5 fw-bold text-primary">{% if can_view_records %}{{ point_records|length }} 次{% else %}<i class="fas fa-lock me-1"></i>受限{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,17 +42,20 @@
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="col">學號</th>
|
||||||
<th scope="col">姓名</th>
|
<th scope="col">姓名</th>
|
||||||
<th scope="col">在學狀態</th>
|
<th scope="col">在學狀態</th>
|
||||||
<th scope="col">學位類別</th>
|
<th scope="col">學位類別</th>
|
||||||
<th scope="col">身分類別</th>
|
<th scope="col">身分類別</th>
|
||||||
<th scope="col">班級/年級</th>
|
<th scope="col">班級/年級</th>
|
||||||
|
<th scope="col">入學日期</th>
|
||||||
{% if user.is_authenticated %}<th scope="col">詳細資料</th>{% endif %}
|
{% if user.is_authenticated %}<th scope="col">詳細資料</th>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in student_data %}
|
{% for item in student_data %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><code>{{ item.student.student_id }}</code></td>
|
||||||
<td class="fw-bold">{{ item.student.name }}</td>
|
<td class="fw-bold">{{ item.student.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.student.is_active %}
|
{% if item.student.is_active %}
|
||||||
|
|
@ -80,11 +83,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.student.grade|default:"未填" }}</td>
|
<td>{{ item.student.grade|default:"未填" }}</td>
|
||||||
|
<td><span class="badge bg-light text-dark border"><i class="far fa-calendar-alt me-1 text-primary"></i>{{ item.student.admission_date|date:"Y-m-d"|default:"--" }}</span></td>
|
||||||
{% if user.is_authenticated %}<td><a href="{% url 'show_attendance' item.student.student_id %}" class="btn btn-sm btn-primary"><i class="fas fa-eye me-1"></i>查看詳情</a></td>{% endif %}
|
{% if user.is_authenticated %}<td><a href="{% url 'show_attendance' item.student.student_id %}" class="btn btn-sm btn-primary"><i class="fas fa-eye me-1"></i>查看詳情</a></td>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{% if user.is_authenticated %}6{% else %}5{% endif %}" class="text-center py-4 text-muted">沒有符合條件的學生資料</td>
|
<td colspan="{% if user.is_authenticated %}8{% else %}7{% endif %}" class="text-center py-4 text-muted">沒有符合條件的學生資料</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="fw-bold">
|
<td class="fw-bold">
|
||||||
{% if user.is_authenticated %}<i
|
{% if user.is_authenticated %}<i
|
||||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{{ student.name }}
|
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{% get_current_language as CURRENT_LANG %}{% if CURRENT_LANG == 'en' and student.foreign_alias %}{{ student.foreign_alias }}{% else %}{{ student.name }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if student.degree_type == '博士生' %}
|
{% if student.degree_type == '博士生' %}
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="fw-bold">
|
<td class="fw-bold">
|
||||||
{% if user.is_authenticated %}<i
|
{% if user.is_authenticated %}<i
|
||||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{{ student.name }}
|
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{% get_current_language as CURRENT_LANG %}{% if CURRENT_LANG == 'en' and student.foreign_alias %}{{ student.foreign_alias }}{% else %}{{ student.name }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if student.degree_type == '博士生' %}
|
{% if student.degree_type == '博士生' %}
|
||||||
|
|
@ -280,7 +280,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in summary_data %}
|
{% for row in summary_data %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">{{ row.student.name }}</td>
|
<td class="fw-bold">{% get_current_language as CURRENT_LANG %}{% if CURRENT_LANG == 'en' and row.student.foreign_alias %}{{ row.student.foreign_alias }}{% else %}{{ row.student.name }}{% endif %}</td>
|
||||||
<td><small class="fw-semibold text-secondary">{{ row.rule_desc }}</small></td>
|
<td><small class="fw-semibold text-secondary">{{ row.rule_desc }}</small></td>
|
||||||
<td>
|
<td>
|
||||||
{% if row.assigned_dates %}
|
{% if row.assigned_dates %}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for student in item.students %}
|
{% for student in item.students %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">{{ student.name }}</td>
|
<td class="fw-bold">{% if student.foreign_alias %}{{ student.foreign_alias }}{% else %}{{ student.name }}{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if student.degree_type == '博士生' %}
|
{% if student.degree_type == '博士生' %}
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for student in item.students %}
|
{% for student in item.students %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">{{ student.name }}</td>
|
<td class="fw-bold">{% if student.foreign_alias %}{{ student.foreign_alias }}{% else %}{{ student.name }}{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if student.degree_type == '博士生' %}
|
{% if student.degree_type == '博士生' %}
|
||||||
<span class="badge bg-danger text-white"><i
|
<span class="badge bg-danger text-white"><i
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.contrib.auth import authenticate, get_user_model
|
from django.contrib.auth import authenticate, get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
@ -59,7 +60,7 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
def test_login_view(self):
|
def test_login_view(self):
|
||||||
response = self.client.get(reverse('login'))
|
response = self.client.get(reverse('login'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'dc1.samdom.mdi.bar')
|
self.assertContains(response, 'Active Directory 登入')
|
||||||
|
|
||||||
def test_parse_group_dn(self):
|
def test_parse_group_dn(self):
|
||||||
dn = 'CN=Domain Admins,CN=Users,DC=samdom,DC=mdi,DC=bar'
|
dn = 'CN=Domain Admins,CN=Users,DC=samdom,DC=mdi,DC=bar'
|
||||||
|
|
@ -96,10 +97,22 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
self.assertContains(response, reverse('user_groups'))
|
self.assertContains(response, reverse('user_groups'))
|
||||||
|
|
||||||
def test_index_page_shows_schedule(self):
|
def test_index_page_shows_schedule(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
response = self.client.get(reverse('index'))
|
response = self.client.get(reverse('index'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, '週會排程')
|
self.assertContains(response, '週會排程')
|
||||||
|
|
||||||
|
def test_pages_require_login_except_static_iframe(self):
|
||||||
|
# Unauthenticated access to protected pages redirects to login
|
||||||
|
for url_name in ['index', 'weekly_schedule', 'student_list', 'show_attendance']:
|
||||||
|
kwargs = {'student_id': 'D12458003'} if url_name == 'show_attendance' else {}
|
||||||
|
response = self.client.get(reverse(url_name, kwargs=kwargs))
|
||||||
|
self.assertEqual(response.status_code, 302, f"URL {url_name} should redirect unauthenticated user")
|
||||||
|
|
||||||
|
# Static iframe view remains publicly accessible
|
||||||
|
response = self.client.get(reverse('weekly_schedule_en_iframe'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_record_attendance_access_unauthenticated(self):
|
def test_record_attendance_access_unauthenticated(self):
|
||||||
response = self.client.get(reverse('record_attendance'))
|
response = self.client.get(reverse('record_attendance'))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
@ -245,6 +258,7 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
self.assertEqual(str(exchange_student), '錢七 (碩士生 / 交換中)')
|
self.assertEqual(str(exchange_student), '錢七 (碩士生 / 交換中)')
|
||||||
|
|
||||||
def test_student_list_active_only_filter(self):
|
def test_student_list_active_only_filter(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
active_student = Student.objects.create(student_id='S010', name='在學學生A', is_active=True)
|
active_student = Student.objects.create(student_id='S010', name='在學學生A', is_active=True)
|
||||||
inactive_student = Student.objects.create(student_id='S011', name='停學生B', is_active=False)
|
inactive_student = Student.objects.create(student_id='S011', name='停學生B', is_active=False)
|
||||||
|
|
||||||
|
|
@ -403,6 +417,7 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
self.assertEqual(imported_student.email2, 'c002@yahoo.com.tw')
|
self.assertEqual(imported_student.email2, 'c002@yahoo.com.tw')
|
||||||
|
|
||||||
def test_weekly_schedule_rules(self):
|
def test_weekly_schedule_rules(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
master_student = Student.objects.create(student_id='M001', name='碩士生A', degree_type='碩士生', student_category='一般生', is_active=True)
|
master_student = Student.objects.create(student_id='M001', name='碩士生A', degree_type='碩士生', student_category='一般生', is_active=True)
|
||||||
doc_reg_student = Student.objects.create(student_id='D001', name='博一般B', degree_type='博士生', student_category='一般生', is_active=True)
|
doc_reg_student = Student.objects.create(student_id='D001', name='博一般B', degree_type='博士生', student_category='一般生', is_active=True)
|
||||||
doc_ins_student = Student.objects.create(student_id='D002', name='博在職C', degree_type='博士生', student_category='在職生', is_active=True)
|
doc_ins_student = Student.objects.create(student_id='D002', name='博在職C', degree_type='博士生', student_category='在職生', is_active=True)
|
||||||
|
|
@ -437,6 +452,7 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
self.assertEqual(WeeklySchedule.objects.filter(student=exchange_student).count(), 0)
|
self.assertEqual(WeeklySchedule.objects.filter(student=exchange_student).count(), 0)
|
||||||
|
|
||||||
def test_weekly_schedule_students_sorted_by_category_and_student_id(self):
|
def test_weekly_schedule_students_sorted_by_category_and_student_id(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from students.models import WeeklySchedule
|
from students.models import WeeklySchedule
|
||||||
test_date = date(2026, 8, 3)
|
test_date = date(2026, 8, 3)
|
||||||
|
|
@ -533,6 +549,7 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
self.assertFalse(WeeklySchedule.objects.filter(id=invalid_card.id).exists())
|
self.assertFalse(WeeklySchedule.objects.filter(id=invalid_card.id).exists())
|
||||||
|
|
||||||
def test_set_language_toggle(self):
|
def test_set_language_toggle(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
response = self.client.get(reverse('set_language', kwargs={'lang_code': 'en'}), follow=True)
|
response = self.client.get(reverse('set_language', kwargs={'lang_code': 'en'}), follow=True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(self.client.session.get('lang'), 'en')
|
self.assertEqual(self.client.session.get('lang'), 'en')
|
||||||
|
|
@ -563,6 +580,11 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
||||||
class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
self.test_user = User.objects.create_user(
|
||||||
|
username='aduser_iframe',
|
||||||
|
password='TestPassword123!',
|
||||||
|
email='aduser_iframe@samdom.mdi.bar'
|
||||||
|
)
|
||||||
self.student = Student.objects.create(
|
self.student = Student.objects.create(
|
||||||
student_id="M999",
|
student_id="M999",
|
||||||
name="Test Student",
|
name="Test Student",
|
||||||
|
|
@ -572,6 +594,7 @@ class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_past_date_empty_card_message(self):
|
def test_past_date_empty_card_message(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
response = self.client.get(reverse('weekly_schedule'))
|
response = self.client.get(reverse('weekly_schedule'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
@ -584,6 +607,7 @@ class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
||||||
self.assertTrue(static_file.exists())
|
self.assertTrue(static_file.exists())
|
||||||
|
|
||||||
def test_bachelor_student_degree_choice_and_scheduling(self):
|
def test_bachelor_student_degree_choice_and_scheduling(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
bachelor = Student.objects.create(
|
bachelor = Student.objects.create(
|
||||||
student_id="B101",
|
student_id="B101",
|
||||||
name="Bachelor Student",
|
name="Bachelor Student",
|
||||||
|
|
@ -610,4 +634,126 @@ class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
||||||
self.assertEqual(past_schedules.count(), 0)
|
self.assertEqual(past_schedules.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class AdmissionDateTestCase(TestCase):
|
||||||
|
def test_b10401005_admission_date_2021(self):
|
||||||
|
from datetime import date
|
||||||
|
student = Student.objects.create(student_id="B10401005", name="張學士")
|
||||||
|
self.assertEqual(student.admission_date, date(2021, 9, 1))
|
||||||
|
|
||||||
|
def test_r15k47005_admission_date_2026(self):
|
||||||
|
from datetime import date
|
||||||
|
student = Student.objects.create(student_id="R15K47005", name="李碩士")
|
||||||
|
self.assertEqual(student.admission_date, date(2026, 9, 1))
|
||||||
|
|
||||||
|
def test_d12458003_admission_date_2023(self):
|
||||||
|
from datetime import date
|
||||||
|
student = Student.objects.create(student_id="D12458003", name="王博士")
|
||||||
|
self.assertEqual(student.admission_date, date(2023, 9, 1))
|
||||||
|
|
||||||
|
def test_custom_admission_date_preserved(self):
|
||||||
|
from datetime import date
|
||||||
|
custom_date = date(2020, 2, 15)
|
||||||
|
student = Student.objects.create(student_id="B10401005", name="趙學士", admission_date=custom_date)
|
||||||
|
self.assertEqual(student.admission_date, custom_date)
|
||||||
|
|
||||||
|
def test_process_excel_rows_auto_sets_admission_date(self):
|
||||||
|
from datetime import date
|
||||||
|
from students.views import process_excel_rows
|
||||||
|
rows = [
|
||||||
|
['學號\nStudent ID', '姓名\nName'],
|
||||||
|
['B10401005', '測試生A'],
|
||||||
|
['R15K47005', '測試生B'],
|
||||||
|
]
|
||||||
|
created, updated = process_excel_rows(rows)
|
||||||
|
self.assertEqual(created, 2)
|
||||||
|
s_a = Student.objects.get(student_id='B10401005')
|
||||||
|
s_b = Student.objects.get(student_id='R15K47005')
|
||||||
|
def test_schedule_only_after_admission_date(self):
|
||||||
|
from datetime import date
|
||||||
|
from students.models import WeeklySchedule
|
||||||
|
from students.views import sync_weekly_schedule_to_db
|
||||||
|
# Future student admitted in 2027
|
||||||
|
future_student = Student.objects.create(student_id="R16K47005", name="未來生", admission_date=date(2027, 9, 1))
|
||||||
|
# Sync schedule for August/September 2026
|
||||||
|
sync_weekly_schedule_to_db(2026, 8, 2026, 9, force=True)
|
||||||
|
# Should have 0 schedules for 2026 because 2026 < admission date 2027-09-01
|
||||||
|
self.assertEqual(WeeklySchedule.objects.filter(student=future_student).count(), 0)
|
||||||
|
|
||||||
|
def test_move_schedule_before_admission_date_rejected(self):
|
||||||
|
from datetime import date
|
||||||
|
from students.models import WeeklySchedule
|
||||||
|
user = User.objects.create_user(username='admin_user', password='password123')
|
||||||
|
self.client.force_login(user)
|
||||||
|
student = Student.objects.create(student_id="R15K47005", name="近期生", admission_date=date(2026, 9, 1))
|
||||||
|
WeeklySchedule.objects.create(student=student, date=date(2026, 9, 7))
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('move_schedule_item'),
|
||||||
|
data=json.dumps({
|
||||||
|
'student_id': 'R15K47005',
|
||||||
|
'from_date': '2026-09-07',
|
||||||
|
'to_date': '2026-08-17'
|
||||||
|
}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('不可將報告日期移轉至入學日期前', response.json().get('error', ''))
|
||||||
|
|
||||||
|
def test_verify_schedule_cleans_cards_before_admission_date(self):
|
||||||
|
from datetime import date
|
||||||
|
from students.models import WeeklySchedule
|
||||||
|
user = User.objects.create_user(username='admin_verify', password='password123')
|
||||||
|
self.client.force_login(user)
|
||||||
|
student = Student.objects.create(student_id="R15K47006", name="校正生", admission_date=date(2026, 9, 1))
|
||||||
|
# Manually create invalid card before admission date
|
||||||
|
invalid_card = WeeklySchedule.objects.create(student=student, date=date(2026, 8, 10))
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
if today <= date(2026, 8, 31):
|
||||||
|
self.client.post(reverse('verify_schedule_rules'))
|
||||||
|
self.assertFalse(WeeklySchedule.objects.filter(id=invalid_card.id).exists())
|
||||||
|
|
||||||
|
|
||||||
|
class AdmissionDateInputFormatsTestCase(TestCase):
|
||||||
|
def test_student_admin_form_accepts_slash_date(self):
|
||||||
|
from datetime import date
|
||||||
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.utils import translation
|
||||||
|
from students.admin import StudentAdmin
|
||||||
|
|
||||||
|
ma = StudentAdmin(Student, AdminSite())
|
||||||
|
Form = ma.get_form(None)
|
||||||
|
|
||||||
|
for locale in ['en', 'zh-hant']:
|
||||||
|
translation.activate(locale)
|
||||||
|
for date_str in ['2025/09/01', '2025-09-01', '2025/9/1', '2025.09.01']:
|
||||||
|
form = Form({
|
||||||
|
'student_id': 'D14K48010',
|
||||||
|
'name': '測試學生',
|
||||||
|
'degree_type': '學士生',
|
||||||
|
'student_category': '一般生',
|
||||||
|
'is_active': True,
|
||||||
|
'admission_date': date_str,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid(), f"Failed for date_str='{date_str}' in locale='{locale}': {form.errors}")
|
||||||
|
self.assertEqual(form.cleaned_data['admission_date'], date(2025, 9, 1))
|
||||||
|
|
||||||
|
def test_import_students_accepts_slash_date(self):
|
||||||
|
from datetime import date
|
||||||
|
from students.views import process_excel_rows
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
['學號', '姓名', '學位類別', '身分類別', '入學日期'],
|
||||||
|
['D14K48099', 'slash日期生', '碩士生', '一般生', '2025/09/01']
|
||||||
|
]
|
||||||
|
created, updated = process_excel_rows(rows)
|
||||||
|
self.assertEqual(created, 1)
|
||||||
|
student = Student.objects.get(student_id='D14K48099')
|
||||||
|
self.assertEqual(student.admission_date, date(2025, 9, 1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ from django.contrib import messages
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponseForbidden, JsonResponse, HttpResponse
|
from django.http import HttpResponseForbidden, JsonResponse, HttpResponse
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
from django.db.models import Q, Case, When, Value, IntegerField
|
from django.db.models import Q, Case, When, Value, IntegerField, F
|
||||||
from .models import Student, PointRecord, WeeklySchedule
|
from .models import Student, PointRecord, WeeklySchedule, calculate_admission_date
|
||||||
from .forms import StudentLoginForm, ADLoginForm
|
from .forms import StudentLoginForm, ADLoginForm
|
||||||
from .ad_backend import fetch_ad_groups
|
from .ad_backend import fetch_ad_groups
|
||||||
|
|
||||||
|
|
@ -242,10 +242,36 @@ def process_excel_rows(rows):
|
||||||
email = email_str
|
email = email_str
|
||||||
break
|
break
|
||||||
|
|
||||||
|
foreign_alias = None
|
||||||
|
for k, v in row_dict.items():
|
||||||
|
if k and ('別名' in str(k) or '外文' in str(k) or 'alias' in str(k).lower()) and v:
|
||||||
|
foreign_alias = str(v).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
admission_date = None
|
||||||
|
for k, v in row_dict.items():
|
||||||
|
if k and ('入學' in str(k) or 'admission' in str(k).lower()) and v:
|
||||||
|
val_str = str(v).strip()
|
||||||
|
try:
|
||||||
|
if hasattr(v, 'date'):
|
||||||
|
admission_date = v.date()
|
||||||
|
else:
|
||||||
|
for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%Y.%m.%d', '%Y年%m月%d日'):
|
||||||
|
try:
|
||||||
|
admission_date = datetime.strptime(val_str, fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
existing_student = Student.objects.filter(student_id=student_id).first()
|
existing_student = Student.objects.filter(student_id=student_id).first()
|
||||||
|
|
||||||
update_defaults = {
|
update_defaults = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'foreign_alias': foreign_alias,
|
||||||
'degree_type': degree_type,
|
'degree_type': degree_type,
|
||||||
'grade': grade,
|
'grade': grade,
|
||||||
'hope_admission_scheme': hope_admission,
|
'hope_admission_scheme': hope_admission,
|
||||||
|
|
@ -257,6 +283,9 @@ def process_excel_rows(rows):
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if admission_date:
|
||||||
|
update_defaults['admission_date'] = admission_date
|
||||||
|
|
||||||
if not existing_student:
|
if not existing_student:
|
||||||
update_defaults['student_category'] = student_category
|
update_defaults['student_category'] = student_category
|
||||||
|
|
||||||
|
|
@ -324,16 +353,37 @@ def sync_weekly_schedule_to_db(m1_year, m1_month, m2_year, m2_month, force=False
|
||||||
valid_m1 = [d for d in m1_mondays if d >= today] or m1_mondays
|
valid_m1 = [d for d in m1_mondays if d >= today] or m1_mondays
|
||||||
valid_m2 = [d for d in m2_mondays if d >= today] or m2_mondays
|
valid_m2 = [d for d in m2_mondays if d >= today] or m2_mondays
|
||||||
|
|
||||||
|
# Remove any existing records that are prior to student's admission date
|
||||||
|
WeeklySchedule.objects.filter(
|
||||||
|
year__in=[m1_year, m2_year],
|
||||||
|
month__in=[m1_month, m2_month],
|
||||||
|
student__admission_date__isnull=False,
|
||||||
|
date__lt=F('student__admission_date')
|
||||||
|
).delete()
|
||||||
|
|
||||||
if not m1_exists or force:
|
if not m1_exists or force:
|
||||||
for idx, student in enumerate(m1_pool):
|
for idx, student in enumerate(m1_pool):
|
||||||
monday_date = valid_m1[idx % len(valid_m1)]
|
valid_mondays_for_student = [
|
||||||
|
d for d in valid_m1
|
||||||
|
if not student.admission_date or d >= student.admission_date
|
||||||
|
]
|
||||||
|
if not valid_mondays_for_student:
|
||||||
|
continue
|
||||||
|
monday_date = valid_mondays_for_student[idx % len(valid_mondays_for_student)]
|
||||||
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
||||||
|
|
||||||
if not m2_exists or force:
|
if not m2_exists or force:
|
||||||
for idx, student in enumerate(m2_pool):
|
for idx, student in enumerate(m2_pool):
|
||||||
monday_date = valid_m2[idx % len(valid_m2)]
|
valid_mondays_for_student = [
|
||||||
|
d for d in valid_m2
|
||||||
|
if not student.admission_date or d >= student.admission_date
|
||||||
|
]
|
||||||
|
if not valid_mondays_for_student:
|
||||||
|
continue
|
||||||
|
monday_date = valid_mondays_for_student[idx % len(valid_mondays_for_student)]
|
||||||
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
||||||
|
|
||||||
|
@login_required(login_url='login')
|
||||||
def index(request):
|
def index(request):
|
||||||
"""Home page view showing the Weekly Seminar Schedule."""
|
"""Home page view showing the Weekly Seminar Schedule."""
|
||||||
return weekly_schedule(request)
|
return weekly_schedule(request)
|
||||||
|
|
@ -360,6 +410,7 @@ def ad_login(request):
|
||||||
form = ADLoginForm()
|
form = ADLoginForm()
|
||||||
return render(request, 'students/login.html', {'form': form})
|
return render(request, 'students/login.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required(login_url='login')
|
||||||
def ad_logout(request):
|
def ad_logout(request):
|
||||||
"""Active Directory Logout View"""
|
"""Active Directory Logout View"""
|
||||||
logout(request)
|
logout(request)
|
||||||
|
|
@ -462,6 +513,7 @@ def recent_attendance(request):
|
||||||
recent_records = PointRecord.objects.select_related('student').order_by('-datetime')[:20]
|
recent_records = PointRecord.objects.select_related('student').order_by('-datetime')[:20]
|
||||||
return render(request, 'students/recent_attendance.html', {'recent_records': recent_records})
|
return render(request, 'students/recent_attendance.html', {'recent_records': recent_records})
|
||||||
|
|
||||||
|
@login_required(login_url='login')
|
||||||
def student_list(request):
|
def student_list(request):
|
||||||
active_only_param = request.GET.get('active_only')
|
active_only_param = request.GET.get('active_only')
|
||||||
if active_only_param is None:
|
if active_only_param is None:
|
||||||
|
|
@ -492,6 +544,7 @@ def student_list(request):
|
||||||
'is_admin_user': is_admin_user,
|
'is_admin_user': is_admin_user,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required(login_url='login')
|
||||||
def weekly_schedule(request):
|
def weekly_schedule(request):
|
||||||
"""
|
"""
|
||||||
Weekly seminar schedule view listing Mondays of current and next months.
|
Weekly seminar schedule view listing Mondays of current and next months.
|
||||||
|
|
@ -632,6 +685,12 @@ def move_schedule_item(request):
|
||||||
if student.student_category == '交換中':
|
if student.student_category == '交換中':
|
||||||
return JsonResponse({'success': False, 'error': '交換生無需報告,無法排程'}, status=400)
|
return JsonResponse({'success': False, 'error': '交換生無需報告,無法排程'}, status=400)
|
||||||
|
|
||||||
|
if student.admission_date and to_date < student.admission_date:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'排程調整限制:該學生入學日期為 {student.admission_date.strftime("%Y-%m-%d")},不可將報告日期移轉至入學日期前 (無法移至 {to_date})。'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
is_monthly = (student.degree_type in ('碩士生', '學士生')) or (student.degree_type == '博士生' and student.student_category == '一般生')
|
is_monthly = (student.degree_type in ('碩士生', '學士生')) or (student.degree_type == '博士生' and student.student_category == '一般生')
|
||||||
|
|
||||||
if is_monthly:
|
if is_monthly:
|
||||||
|
|
@ -655,7 +714,6 @@ def move_schedule_item(request):
|
||||||
log_msg = f"AUDIT LOG: User '{request.user.username}' (authenticated) moved weekly schedule for student '{student.student_id}' ({student.name}) from {from_date} to {to_date}."
|
log_msg = f"AUDIT LOG: User '{request.user.username}' (authenticated) moved weekly schedule for student '{student.student_id}' ({student.name}) from {from_date} to {to_date}."
|
||||||
logger.info(log_msg)
|
logger.info(log_msg)
|
||||||
schedule_logger.info(log_msg)
|
schedule_logger.info(log_msg)
|
||||||
print(log_msg)
|
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|
@ -671,7 +729,7 @@ def move_schedule_item(request):
|
||||||
def verify_schedule_rules(request):
|
def verify_schedule_rules(request):
|
||||||
"""
|
"""
|
||||||
Verify and clean up WeeklySchedule records based on frequency rules:
|
Verify and clean up WeeklySchedule records based on frequency rules:
|
||||||
- Rule 1: Delete all schedule cards for Exchange students (交換中) or inactive students.
|
- Rule 1: Delete all schedule cards for Exchange students (交換中), inactive students, or dates before admission_date.
|
||||||
- Rule 2: 碩士生 & 博士一般生 must have exactly 1 report per month. Delete excess cards, create missing cards.
|
- Rule 2: 碩士生 & 博士一般生 must have exactly 1 report per month. Delete excess cards, create missing cards.
|
||||||
- Rule 3: 博士在職生 must have exactly 1 report per 2 months. Delete excess cards, create missing cards.
|
- Rule 3: 博士在職生 must have exactly 1 report per 2 months. Delete excess cards, create missing cards.
|
||||||
Logs user action and results.
|
Logs user action and results.
|
||||||
|
|
@ -690,13 +748,14 @@ def verify_schedule_rules(request):
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
created_count = 0
|
created_count = 0
|
||||||
|
|
||||||
# 1. Delete all cards for Exchange students (交換中) or inactive students
|
# 1. Delete cards for Exchange students (交換中), inactive students, or dates before admission_date
|
||||||
from django.db.models import Q
|
|
||||||
invalid_records = WeeklySchedule.objects.filter(
|
invalid_records = WeeklySchedule.objects.filter(
|
||||||
year__in=[m1_year, m2_year],
|
year__in=[m1_year, m2_year],
|
||||||
month__in=[m1_month, m2_month]
|
month__in=[m1_month, m2_month]
|
||||||
).filter(
|
).filter(
|
||||||
Q(student__student_category='交換中') | Q(student__is_active=False)
|
Q(student__student_category='交換中') |
|
||||||
|
Q(student__is_active=False) |
|
||||||
|
Q(student__admission_date__isnull=False, date__lt=F('student__admission_date'))
|
||||||
)
|
)
|
||||||
deleted_count += invalid_records.count()
|
deleted_count += invalid_records.count()
|
||||||
invalid_records.delete()
|
invalid_records.delete()
|
||||||
|
|
@ -710,6 +769,10 @@ def verify_schedule_rules(request):
|
||||||
for s in active_students:
|
for s in active_students:
|
||||||
is_monthly = (s.degree_type in ('碩士生', '學士生')) or (s.degree_type == '博士生' and s.student_category == '一般生')
|
is_monthly = (s.degree_type in ('碩士生', '學士生')) or (s.degree_type == '博士生' and s.student_category == '一般生')
|
||||||
|
|
||||||
|
valid_m1_for_s = [d for d in valid_m1_mondays if not s.admission_date or d >= s.admission_date]
|
||||||
|
valid_m2_for_s = [d for d in valid_m2_mondays if not s.admission_date or d >= s.admission_date]
|
||||||
|
valid_all_for_s = [d for d in valid_all_mondays if not s.admission_date or d >= s.admission_date]
|
||||||
|
|
||||||
if is_monthly:
|
if is_monthly:
|
||||||
# Check Month 1
|
# Check Month 1
|
||||||
m1_records = list(WeeklySchedule.objects.filter(student=s, year=m1_year, month=m1_month).order_by('date'))
|
m1_records = list(WeeklySchedule.objects.filter(student=s, year=m1_year, month=m1_month).order_by('date'))
|
||||||
|
|
@ -718,8 +781,8 @@ def verify_schedule_rules(request):
|
||||||
r.delete()
|
r.delete()
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
elif len(m1_records) == 0:
|
elif len(m1_records) == 0:
|
||||||
if valid_m1_mondays:
|
if valid_m1_for_s:
|
||||||
monday = min(valid_m1_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
monday = min(valid_m1_for_s, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||||
WeeklySchedule.objects.create(student=s, date=monday)
|
WeeklySchedule.objects.create(student=s, date=monday)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
|
|
||||||
|
|
@ -730,8 +793,8 @@ def verify_schedule_rules(request):
|
||||||
r.delete()
|
r.delete()
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
elif len(m2_records) == 0:
|
elif len(m2_records) == 0:
|
||||||
if valid_m2_mondays:
|
if valid_m2_for_s:
|
||||||
monday = min(valid_m2_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
monday = min(valid_m2_for_s, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||||
WeeklySchedule.objects.create(student=s, date=monday)
|
WeeklySchedule.objects.create(student=s, date=monday)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
else:
|
else:
|
||||||
|
|
@ -742,15 +805,14 @@ def verify_schedule_rules(request):
|
||||||
r.delete()
|
r.delete()
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
elif len(bimonthly_records) == 0:
|
elif len(bimonthly_records) == 0:
|
||||||
if valid_all_mondays:
|
if valid_all_for_s:
|
||||||
monday = min(valid_all_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
monday = min(valid_all_for_s, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||||
WeeklySchedule.objects.create(student=s, date=monday)
|
WeeklySchedule.objects.create(student=s, date=monday)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
|
|
||||||
log_msg = f"AUDIT LOG: User '{request.user.username}' executed Verify & Clean Schedule Rules. Deleted {deleted_count} non-compliant/exchange cards, added {created_count} missing cards."
|
log_msg = f"AUDIT LOG: User '{request.user.username}' executed Verify & Clean Schedule Rules. Deleted {deleted_count} non-compliant/exchange cards, added {created_count} missing cards."
|
||||||
logger.info(log_msg)
|
logger.info(log_msg)
|
||||||
schedule_logger.info(log_msg)
|
schedule_logger.info(log_msg)
|
||||||
print(log_msg)
|
|
||||||
|
|
||||||
# Update static English HTML for iframe embed
|
# Update static English HTML for iframe embed
|
||||||
generate_english_schedule_static_html()
|
generate_english_schedule_static_html()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue