feat: implement dynamic weekly meeting scheduling system with drag-and-drop management and automated rule validation
This commit is contained in:
parent
89428f773b
commit
c25a3afc59
14 changed files with 2741 additions and 115 deletions
|
|
@ -157,7 +157,7 @@ msgid "免報告"
|
|||
msgstr "Exempt"
|
||||
|
||||
msgid "次"
|
||||
msgstr "time(s)"
|
||||
msgstr "times"
|
||||
|
||||
msgid "拖曳排程微調提示:"
|
||||
msgstr "Drag & Drop Tip: "
|
||||
|
|
@ -170,3 +170,60 @@ msgstr "View-Only Mode: "
|
|||
|
||||
msgid "登入系統後,可拖曳學生調整報告日期。"
|
||||
msgstr "Log in to drag and drop student cards to adjust presentation dates."
|
||||
|
||||
msgid "不可將報告日期移轉至早於今天"
|
||||
msgstr "Cannot move schedule date to earlier than today"
|
||||
|
||||
msgid "當日無排定報告者 (可拖曳學生至此)"
|
||||
msgstr "No presenter scheduled for this day (drag & drop student here)"
|
||||
|
||||
msgid "學士生"
|
||||
msgstr "Undergraduate Student"
|
||||
|
||||
msgid "學士生 (1次/月)"
|
||||
msgstr "Undergraduate Student (1 time/month)"
|
||||
|
||||
msgid "%(y1)s年%(m1)s月 & %(y2)s年%(m2)s月"
|
||||
msgstr "%(y1)s-%(m1)s & %(y2)s-%(m2)s"
|
||||
|
||||
msgid "%(year)s 年 %(month)s 月週會排程 (當月)"
|
||||
msgstr "%(year)s-%(month)s Schedule (Current Month)"
|
||||
|
||||
msgid "%(year)s 年 %(month)s 月週會排程 (下月)"
|
||||
msgstr "%(year)s-%(month)s Schedule (Next Month)"
|
||||
|
||||
msgid "%(date_str)s (星期一)"
|
||||
msgstr "%(date_str)s (Monday)"
|
||||
|
||||
msgid "%(count)s 位報告者"
|
||||
msgstr "%(count)s presenters"
|
||||
|
||||
msgid "%(count)s 位報告者(單數)"
|
||||
msgstr "%(count)s presenter"
|
||||
|
||||
msgid "經調整後但仍未能報告者,須準備一小時之進度簡報,當週向指導教授當面報告,次週週會補報。若連續兩次應報告而未報告,將視同解除指導關係。"
|
||||
msgstr "Students who fail to present after schedule adjustment must prepare a 1-hour progress report, present in person to their advisor that week, and make up the presentation at next week's seminar. Failing to present twice consecutively will be treated as termination of the advising relationship."
|
||||
|
||||
msgid "確定要校正並驗證排程規則嗎?(如為交換生將自動刪除其卡片,並清理重複或補齊缺漏排程)"
|
||||
msgstr "Are you sure you want to verify and clean schedule rules? Exchange students' cards will be deleted, and missing/duplicate schedules will be cleaned up."
|
||||
|
||||
msgid "博士在職生 (1次/2月)"
|
||||
msgstr "Ph.D. In-Service Student (1 time/2 months)"
|
||||
|
||||
msgid "一般生 (1次/月)"
|
||||
msgstr "Regular Student (1 time/month)"
|
||||
|
||||
msgid "0 次"
|
||||
msgstr "0 time"
|
||||
|
||||
msgid "%(count)s 次"
|
||||
msgstr "%(count)s times"
|
||||
|
||||
msgid "%(count)s 次(單數)"
|
||||
msgstr "%(count)s time"
|
||||
|
||||
msgid "無在學學生資料"
|
||||
msgstr "No active student data"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,3 +35,9 @@ msgstr "登出"
|
|||
|
||||
msgid "AD 登入"
|
||||
msgstr "AD 登入"
|
||||
|
||||
msgid "%(count)s 位報告者(單數)"
|
||||
msgstr "%(count)s 位報告者"
|
||||
|
||||
msgid "%(count)s 次(單數)"
|
||||
msgstr "%(count)s 次"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ DEBUG = True
|
|||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'https://point.mdi.bar',
|
||||
'http://point.mdi.bar',
|
||||
'https://*.mdi.bar',
|
||||
'http://*.mdi.bar',
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
]
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
|
|
@ -141,6 +152,9 @@ LOCALE_PATHS = [
|
|||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
]
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
|
|
|||
1040
point_system/static/weekly_schedule_en.html
Normal file
1040
point_system/static/weekly_schedule_en.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2 on 2026-08-04 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('students', '0006_alter_weeklyschedule_options_student_email2_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='student',
|
||||
name='degree_type',
|
||||
field=models.CharField(choices=[('學士生', '學士生'), ('碩士生', '碩士生'), ('博士生', '博士生')], default='碩士生', max_length=10, verbose_name='學位類別'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from django.db import models
|
||||
|
||||
DEGREE_CHOICES = [
|
||||
('學士生', '學士生'),
|
||||
('碩士生', '碩士生'),
|
||||
('博士生', '博士生'),
|
||||
]
|
||||
|
|
|
|||
1040
point_system/students/static/students/weekly_schedule_en.html
Normal file
1040
point_system/students/static/students/weekly_schedule_en.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ student.name }} ({{ student.student_id }}) - 考勤紀錄 - 1218 Lab{% endblock %}
|
||||
|
||||
|
|
@ -34,9 +35,11 @@
|
|||
<small class="text-muted d-block mb-1">學位 / 身分</small>
|
||||
<div>
|
||||
{% if student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white"><i class="fas fa-user-graduate me-1"></i>博士生</span>
|
||||
<span class="badge bg-danger text-white"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||
{% elif 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>碩士生</span>
|
||||
<span class="badge bg-info text-dark"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if student.student_category == '在職生' %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<!-- 在 templates/students/student_list.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}學生列表{% endblock %}
|
||||
|
||||
|
|
@ -41,20 +42,18 @@
|
|||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-dark">
|
||||
<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>
|
||||
{% if user.is_authenticated %}<th scope="col">詳細資料</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in student_data %}
|
||||
<tr>
|
||||
<td class="fw-bold"><code>{{ item.student.student_id }}</code></td>
|
||||
<td>{{ item.student.name }}</td>
|
||||
<td class="fw-bold">{{ item.student.name }}</td>
|
||||
<td>
|
||||
{% if item.student.is_active %}
|
||||
<span class="badge bg-success text-white"><i class="fas fa-check me-1"></i>在學</span>
|
||||
|
|
@ -64,9 +63,11 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if item.student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white px-2 py-1"><i class="fas fa-user-graduate me-1"></i>博士生</span>
|
||||
<span class="badge bg-danger text-white px-2 py-1"><i class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||
{% elif item.student.degree_type == '學士生' %}
|
||||
<span class="badge bg-primary text-white px-2 py-1"><i class="fas fa-user-graduate me-1"></i>{% trans "學士生" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info text-dark px-2 py-1"><i class="fas fa-graduation-cap me-1"></i>碩士生</span>
|
||||
<span class="badge bg-info text-dark px-2 py-1"><i class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
|
@ -79,11 +80,11 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.student.grade|default:"未填" }}</td>
|
||||
<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>
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">沒有符合條件的學生資料</td>
|
||||
<td colspan="{% if user.is_authenticated %}6{% else %}5{% endif %}" class="text-center py-4 text-muted">沒有符合條件的學生資料</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}週會排程 - 1218 Lab{% endblock %}
|
||||
{% block title %}{% trans "週會排程" %} - 1218 Lab{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
|
||||
|
|
@ -13,13 +13,13 @@
|
|||
<i class="fas fa-database me-1"></i>{% trans "資料庫狀態: 已同步儲存" %}
|
||||
</span>
|
||||
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
|
||||
<i class="fas fa-clock me-1"></i>{{ m1_year }}年{{ m1_month }}月 & {{ m2_year }}年{{ m2_month }}月
|
||||
<i class="fas fa-clock me-1"></i>{% blocktrans with y1=m1_year m1=m1_month y2=m2_year m2=m2_month %}{{ y1 }}年{{ m1 }}月 & {{ y2 }}年{{ m2 }}月{% endblocktrans %}
|
||||
</span>
|
||||
{% if user.is_authenticated %}
|
||||
<form method="post" action="{% url 'verify_schedule_rules' %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-warning btn-sm shadow-sm text-dark fw-bold"
|
||||
onclick="return confirm('確定要校正並驗證排程規則嗎?(如為交換生將自動刪除其卡片,並清理重複或補齊缺漏排程)');">
|
||||
onclick="return confirm('{% trans "確定要校正並驗證排程規則嗎?(如為交換生將自動刪除其卡片,並清理重複或補齊缺漏排程)" %}');">
|
||||
<i class="fas fa-check-double me-1"></i>{% trans "校正並驗證排程規則" %}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -31,26 +31,32 @@
|
|||
<div class="card shadow-sm border-0 rounded-3 mb-4 bg-light">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold text-dark mb-3"><i class="fas fa-info-circle text-primary me-2"></i>{% trans "週會報告排程規則" %}</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-info shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-graduation-cap text-info me-1"></i>{% trans "碩士生 (一般 & 在職)" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-danger shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-user-graduate text-danger me-1"></i>{% trans "博士一般生" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="row g-3 row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-5">
|
||||
<div class="col">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-warning shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-user-tie text-warning me-1"></i>{% trans "博士在職生" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每 2 個月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="col">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-danger shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-user-graduate text-danger me-1"></i>{% trans "博士一般生" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-info shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-graduation-cap text-info me-1"></i>{% trans "碩士生 (一般 & 在職)" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-primary shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-user-graduate text-primary me-1"></i>{% trans "學士生" %}</div>
|
||||
<small class="text-muted">{% trans "排程頻率: 每月 1 次" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="p-3 bg-white rounded border border-start border-4 border-secondary shadow-sm h-100">
|
||||
<div class="fw-bold text-dark mb-1"><i class="fas fa-exchange-alt text-secondary me-1"></i>{% trans "交換生 (交換中)" %}</div>
|
||||
<small class="text-muted">{% trans "排程狀態: 免報告" %}</small>
|
||||
|
|
@ -80,7 +86,7 @@
|
|||
<!-- 當月週會排程 (Month 1) -->
|
||||
<div class="mb-5">
|
||||
<h3 class="h4 text-primary border-bottom border-primary border-2 pb-2 mb-4">
|
||||
<i class="fas fa-calendar-day me-2"></i>{{ m1_year }} 年 {{ m1_month }} 月週會排程 (當月)
|
||||
<i class="fas fa-calendar-day me-2"></i>{% blocktrans with year=m1_year month=m1_month %}{{ year }} 年 {{ month }} 月週會排程 (當月){% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4">
|
||||
{% for item in schedule_m1 %}
|
||||
|
|
@ -89,9 +95,15 @@
|
|||
style="transition: all 0.2s ease-in-out;">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-check me-2"></i>{{ item.formatted }} (星期一)
|
||||
<i class="fas fa-calendar-check me-2"></i>{% blocktrans with date_str=item.formatted %}{{ date_str }} (星期一){% endblocktrans %}
|
||||
</h5>
|
||||
<span class="badge bg-light text-primary fs-6">{{ item.students|length }} 位報告者</span>
|
||||
<span class="badge bg-light text-primary fs-6">
|
||||
{% if item.students|length > 1 %}
|
||||
{% blocktrans with count=item.students|length %}{{ count }} 位報告者{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with count=item.students|length %}{{ count }} 位報告者(單數){% endblocktrans %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if item.students %}
|
||||
|
|
@ -99,10 +111,9 @@
|
|||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 25%;">學號</th>
|
||||
<th scope="col" style="width: 25%;">姓名</th>
|
||||
<th scope="col" style="width: 25%;">學位類別</th>
|
||||
<th scope="col" style="width: 25%;">身分類別</th>
|
||||
<th scope="col" style="width: 34%;">{% trans "姓名" %}</th>
|
||||
<th scope="col" style="width: 33%;">{% trans "學位類別" %}</th>
|
||||
<th scope="col" style="width: 33%;">{% trans "身分類別" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -116,29 +127,30 @@
|
|||
{% endif %}
|
||||
<td class="fw-bold">
|
||||
{% if user.is_authenticated %}<i
|
||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}
|
||||
<code>{{ student.student_id }}</code>
|
||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{{ student.name }}
|
||||
</td>
|
||||
<td>{{ student.name }}</td>
|
||||
<td>
|
||||
{% if student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>博士生</span>
|
||||
class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||
{% elif 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>碩士生</span>
|
||||
class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if student.student_category == '在職生' %}
|
||||
<span class="badge bg-warning text-dark"><i
|
||||
class="fas fa-user-tie me-1"></i>在職生</span>
|
||||
class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||
{% elif student.student_category == '交換中' %}
|
||||
<span class="badge bg-secondary text-white"><i
|
||||
class="fas fa-exchange-alt me-1"></i>交換中</span>
|
||||
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>一般生</span>
|
||||
class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -147,7 +159,11 @@
|
|||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">當日無排定報告者 (可拖曳學生至此)</p>
|
||||
{% if item.is_past %}
|
||||
<p class="text-center text-muted py-4 mb-0"><i class="fas fa-ban me-1 text-danger"></i>{% trans "不可將報告日期移轉至早於今天" %}</p>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">{% trans "當日無排定報告者 (可拖曳學生至此)" %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,7 +175,7 @@
|
|||
<!-- 下月週會排程 (Month 2) -->
|
||||
<div class="mb-5">
|
||||
<h3 class="h4 text-success border-bottom border-success border-2 pb-2 mb-4">
|
||||
<i class="fas fa-calendar-week me-2"></i>{{ m2_year }} 年 {{ m2_month }} 月週會排程 (下月)
|
||||
<i class="fas fa-calendar-week me-2"></i>{% blocktrans with year=m2_year month=m2_month %}{{ year }} 年 {{ month }} 月週會排程 (下月){% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4">
|
||||
{% for item in schedule_m2 %}
|
||||
|
|
@ -168,9 +184,15 @@
|
|||
style="transition: all 0.2s ease-in-out;">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-check me-2"></i>{{ item.formatted }} (星期一)
|
||||
<i class="fas fa-calendar-check me-2"></i>{% blocktrans with date_str=item.formatted %}{{ date_str }} (星期一){% endblocktrans %}
|
||||
</h5>
|
||||
<span class="badge bg-light text-success fs-6">{{ item.students|length }} 位報告者</span>
|
||||
<span class="badge bg-light text-success fs-6">
|
||||
{% if item.students|length > 1 %}
|
||||
{% blocktrans with count=item.students|length %}{{ count }} 位報告者{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with count=item.students|length %}{{ count }} 位報告者(單數){% endblocktrans %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if item.students %}
|
||||
|
|
@ -178,10 +200,9 @@
|
|||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 25%;">學號</th>
|
||||
<th scope="col" style="width: 25%;">姓名</th>
|
||||
<th scope="col" style="width: 25%;">學位類別</th>
|
||||
<th scope="col" style="width: 25%;">身分類別</th>
|
||||
<th scope="col" style="width: 34%;">{% trans "姓名" %}</th>
|
||||
<th scope="col" style="width: 33%;">{% trans "學位類別" %}</th>
|
||||
<th scope="col" style="width: 33%;">{% trans "身分類別" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -195,29 +216,30 @@
|
|||
{% endif %}
|
||||
<td class="fw-bold">
|
||||
{% if user.is_authenticated %}<i
|
||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}
|
||||
<code>{{ student.student_id }}</code>
|
||||
class="fas fa-grip-vertical text-muted me-2"></i>{% endif %}{{ student.name }}
|
||||
</td>
|
||||
<td>{{ student.name }}</td>
|
||||
<td>
|
||||
{% if student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>博士生</span>
|
||||
class="fas fa-user-graduate me-1"></i>{% trans "博士生" %}</span>
|
||||
{% elif 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>碩士生</span>
|
||||
class="fas fa-graduation-cap me-1"></i>{% trans "碩士生" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if student.student_category == '在職生' %}
|
||||
<span class="badge bg-warning text-dark"><i
|
||||
class="fas fa-user-tie me-1"></i>在職生</span>
|
||||
class="fas fa-user-tie me-1"></i>{% trans "在職生" %}</span>
|
||||
{% elif student.student_category == '交換中' %}
|
||||
<span class="badge bg-secondary text-white"><i
|
||||
class="fas fa-exchange-alt me-1"></i>交換中</span>
|
||||
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>一般生</span>
|
||||
class="fas fa-user me-1"></i>{% trans "一般生" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -226,7 +248,11 @@
|
|||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">當日無排定報告者 (可拖曳學生至此)</p>
|
||||
{% if item.is_past %}
|
||||
<p class="text-center text-muted py-4 mb-0"><i class="fas fa-ban me-1 text-danger"></i>{% trans "不可將報告日期移轉至早於今天" %}</p>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">{% trans "當日無排定報告者 (可拖曳學生至此)" %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -238,47 +264,46 @@
|
|||
<!-- 學生個人報告總覽與統計 -->
|
||||
<div class="card shadow-sm border-0 rounded-3 mt-5">
|
||||
<div class="card-header bg-dark text-white py-3">
|
||||
<h5 class="mb-0"><i class="fas fa-tasks me-2"></i>學生個人報告統計明細 (兩月排程核對)</h5>
|
||||
<h5 class="mb-0"><i class="fas fa-tasks me-2"></i>{% trans "學生個人報告統計明細 (兩月排程核對)" %}</h5>
|
||||
</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>學號</th>
|
||||
<th>姓名</th>
|
||||
<th>身份 / 規則</th>
|
||||
<th>排定報告日期 (Mondays)</th>
|
||||
<th>應報告次數</th>
|
||||
<th>{% trans "姓名" %}</th>
|
||||
<th>{% trans "身份 / 規則" %}</th>
|
||||
<th>{% trans "排定報告日期 (Mondays)" %}</th>
|
||||
<th>{% trans "應報告次數" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in summary_data %}
|
||||
<tr>
|
||||
<td class="fw-bold"><code>{{ row.student.student_id }}</code></td>
|
||||
<td>{{ row.student.name }}</td>
|
||||
<td class="fw-bold">{{ row.student.name }}</td>
|
||||
<td><small class="fw-semibold text-secondary">{{ row.rule_desc }}</small></td>
|
||||
<td>
|
||||
{% if row.assigned_dates %}
|
||||
{% for d in row.assigned_dates %}
|
||||
<span class="badge bg-primary me-1 mb-1"><i class="far fa-calendar-alt me-1"></i>{{ d
|
||||
}}</span>
|
||||
<span class="badge bg-primary me-1 mb-1"><i class="far fa-calendar-alt me-1"></i>{{ d }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary"><i class="fas fa-minus-circle me-1"></i>免報告</span>
|
||||
<span class="badge bg-secondary"><i class="fas fa-minus-circle me-1"></i>{% trans "免報告" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if row.total_count > 0 %}
|
||||
<span class="badge bg-success fs-6">{{ row.total_count }} 次</span>
|
||||
{% if row.total_count > 1 %}
|
||||
<span class="badge bg-success fs-6">{% blocktrans with count=row.total_count %}{{ count }} 次{% endblocktrans %}</span>
|
||||
{% elif row.total_count == 1 %}
|
||||
<span class="badge bg-success fs-6">{% blocktrans with count=row.total_count %}{{ count }} 次(單數){% endblocktrans %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary fs-6">0 次</span>
|
||||
<span class="badge bg-secondary fs-6">{% trans "0 次" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">無在學學生資料</td>
|
||||
<td colspan="4" class="text-center py-4 text-muted">{% trans "無在學學生資料" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Weekly Seminar Schedule</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
padding: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.table code {
|
||||
color: #0d6efd;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid p-0">
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Month 1 Schedule -->
|
||||
<div class="mb-5">
|
||||
<h3 class="h4 text-primary border-bottom border-primary border-2 pb-2 mb-4">
|
||||
<i class="fas fa-calendar-day me-2"></i>{{ m1_year }}-{{ m1_month|stringformat:"02d" }} Schedule
|
||||
(Current Month)
|
||||
</h3>
|
||||
<div class="row g-4">
|
||||
{% for item in schedule_m1 %}
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div
|
||||
class="card-header bg-primary text-white d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-check me-2"></i>{{ item.formatted }} (Monday)
|
||||
</h5>
|
||||
<span class="badge bg-light text-primary fs-6">{% if item.students|length > 1 %}{{ item.students|length }} presenters{% else %}{{ item.students|length }} presenter{% endif %}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if item.students %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 34%;">Name</th>
|
||||
<th scope="col" style="width: 33%;">Degree Level</th>
|
||||
<th scope="col" style="width: 33%;">Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for student in item.students %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ student.name }}</td>
|
||||
<td>
|
||||
{% if student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
||||
{% elif student.degree_type == '學士生' %}
|
||||
<span class="badge bg-primary text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>Undergraduate
|
||||
Student</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info text-dark"><i
|
||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if student.student_category == '在職生' %}
|
||||
<span class="badge bg-warning text-dark"><i
|
||||
class="fas fa-user-tie me-1"></i>In-Service</span>
|
||||
{% elif student.student_category == '交換中' %}
|
||||
<span class="badge bg-secondary text-white"><i
|
||||
class="fas fa-exchange-alt me-1"></i>On Exchange</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success text-white"><i
|
||||
class="fas fa-user me-1"></i>Regular</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if item.is_past %}
|
||||
<p class="text-center text-muted py-4 mb-0"><i
|
||||
class="fas fa-ban me-1 text-danger"></i>Cannot move schedule date to earlier than
|
||||
today</p>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">No presenter scheduled for this day</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Month 2 Schedule -->
|
||||
<div class="mb-5">
|
||||
<h3 class="h4 text-success border-bottom border-success border-2 pb-2 mb-4">
|
||||
<i class="fas fa-calendar-week me-2"></i>{{ m2_year }}-{{ m2_month|stringformat:"02d" }} Schedule (Next
|
||||
Month)
|
||||
</h3>
|
||||
<div class="row g-4">
|
||||
{% for item in schedule_m2 %}
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div
|
||||
class="card-header bg-success text-white d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-check me-2"></i>{{ item.formatted }} (Monday)
|
||||
</h5>
|
||||
<span class="badge bg-light text-success fs-6">{% if item.students|length > 1 %}{{ item.students|length }} presenters{% else %}{{ item.students|length }} presenter{% endif %}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if item.students %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 34%;">Name</th>
|
||||
<th scope="col" style="width: 33%;">Degree Level</th>
|
||||
<th scope="col" style="width: 33%;">Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for student in item.students %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ student.name }}</td>
|
||||
<td>
|
||||
{% if student.degree_type == '博士生' %}
|
||||
<span class="badge bg-danger text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>Ph.D. Student</span>
|
||||
{% elif student.degree_type == '學士生' %}
|
||||
<span class="badge bg-primary text-white"><i
|
||||
class="fas fa-user-graduate me-1"></i>Undergraduate
|
||||
Student</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info text-dark"><i
|
||||
class="fas fa-graduation-cap me-1"></i>Master's Student</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if student.student_category == '在職生' %}
|
||||
<span class="badge bg-warning text-dark"><i
|
||||
class="fas fa-user-tie me-1"></i>In-Service</span>
|
||||
{% elif student.student_category == '交換中' %}
|
||||
<span class="badge bg-secondary text-white"><i
|
||||
class="fas fa-exchange-alt me-1"></i>On Exchange</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success text-white"><i
|
||||
class="fas fa-user me-1"></i>Regular</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if item.is_past %}
|
||||
<p class="text-center text-muted py-4 mb-0"><i
|
||||
class="fas fa-ban me-1 text-danger"></i>Cannot move schedule date to earlier than
|
||||
today</p>
|
||||
{% else %}
|
||||
<p class="text-center text-muted py-4 mb-0">No presenter scheduled for this day</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from django.test import TestCase, Client
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
from students.models import Student
|
||||
from students.ad_backend import ActiveDirectoryBackend, parse_group_dn
|
||||
|
|
@ -182,7 +183,12 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
|||
s = Student.objects.create(student_id='D12458003', name='測試生', degree_type='博士生', is_active=True)
|
||||
PointRecord.objects.create(student=s)
|
||||
|
||||
# Unauthenticated / non-admin user -> 簽到歷史紀錄 hidden with permission notice
|
||||
# Unauthenticated user -> redirected to login
|
||||
response = self.client.get(reverse('show_attendance', kwargs={'student_id': 'D12458003'}))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Authenticated non-admin user -> 簽到歷史紀錄 hidden with permission notice
|
||||
self.client.force_login(self.test_user)
|
||||
response = self.client.get(reverse('show_attendance', kwargs={'student_id': 'D12458003'}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'D12458003')
|
||||
|
|
@ -553,3 +559,55 @@ class ActiveDirectoryBackendTestCase(TestCase):
|
|||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
self.assertIn("AUDIT LOG: User 'aduser' executed Verify & Clean Schedule Rules", content)
|
||||
|
||||
class EnglishStaticIframeAndPastEmptyCardTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.student = Student.objects.create(
|
||||
student_id="M999",
|
||||
name="Test Student",
|
||||
degree_type="碩士生",
|
||||
student_category="一般生",
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def test_past_date_empty_card_message(self):
|
||||
response = self.client.get(reverse('weekly_schedule'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_generate_english_schedule_static_html_and_iframe_endpoint(self):
|
||||
response = self.client.get(reverse('weekly_schedule_en_iframe'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Weekly Seminar Schedule", response.content.decode('utf-8'))
|
||||
|
||||
static_file = settings.BASE_DIR / 'static' / 'weekly_schedule_en.html'
|
||||
self.assertTrue(static_file.exists())
|
||||
|
||||
def test_bachelor_student_degree_choice_and_scheduling(self):
|
||||
bachelor = Student.objects.create(
|
||||
student_id="B101",
|
||||
name="Bachelor Student",
|
||||
degree_type="學士生",
|
||||
student_category="一般生",
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(bachelor.degree_type, "學士生")
|
||||
|
||||
response = self.client.get(reverse('weekly_schedule'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("學士生", response.content.decode('utf-8'))
|
||||
|
||||
def test_verify_schedule_rules_never_adds_past_dates(self):
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
# Delete schedules for a student
|
||||
s = Student.objects.create(student_id="M888", name="New M Student", degree_type="碩士生", student_category="一般生", is_active=True)
|
||||
# Verify rules
|
||||
self.client.post(reverse('verify_schedule_rules'))
|
||||
# Check all schedules for this student
|
||||
from students.models import WeeklySchedule
|
||||
past_schedules = WeeklySchedule.objects.filter(student=s, date__lt=today)
|
||||
self.assertEqual(past_schedules.count(), 0)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ urlpatterns = [
|
|||
path('schedule/', views.weekly_schedule, name='weekly_schedule'),
|
||||
path('schedule/move/', views.move_schedule_item, name='move_schedule_item'),
|
||||
path('schedule/verify-rules/', views.verify_schedule_rules, name='verify_schedule_rules'),
|
||||
path('schedule/embed/en/', views.weekly_schedule_en_iframe, name='weekly_schedule_en_iframe'),
|
||||
path('update-from-excel/', views.update_students_from_excel, name='update_students_from_excel'),
|
||||
path('upload-excel/', views.upload_students_excel, name='upload_students_excel'),
|
||||
path('set-language/<str:lang_code>/', views.set_language, name='set_language'),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ from django.contrib.auth import authenticate, login, logout
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseForbidden, JsonResponse
|
||||
from django.http import HttpResponseForbidden, JsonResponse, HttpResponse
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.db.models import Q, Case, When, Value, IntegerField
|
||||
from .models import Student, PointRecord, WeeklySchedule
|
||||
from .forms import StudentLoginForm, ADLoginForm
|
||||
|
|
@ -22,6 +23,7 @@ def get_month_mondays(year, month):
|
|||
return [d for d in c.itermonthdates(year, month) if d.month == month and d.weekday() == 0]
|
||||
|
||||
from django.utils import translation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
def set_language(request, lang_code):
|
||||
"""
|
||||
|
|
@ -171,6 +173,8 @@ def process_excel_rows(rows):
|
|||
else:
|
||||
if student_id.upper().startswith('D'):
|
||||
degree_type = '博士生'
|
||||
elif student_id.upper().startswith('B'):
|
||||
degree_type = '學士生'
|
||||
|
||||
student_category = '一般生'
|
||||
for k, v in row_dict.items():
|
||||
|
|
@ -301,7 +305,7 @@ def sync_weekly_schedule_to_db(m1_year, m1_month, m2_year, m2_month, force=False
|
|||
for s in active_students:
|
||||
if s.student_category == '交換中':
|
||||
continue
|
||||
elif s.degree_type == '碩士生':
|
||||
elif s.degree_type in ('碩士生', '學士生'):
|
||||
monthly_students.append(s)
|
||||
elif s.degree_type == '博士生' and s.student_category == '一般生':
|
||||
monthly_students.append(s)
|
||||
|
|
@ -316,14 +320,18 @@ def sync_weekly_schedule_to_db(m1_year, m1_month, m2_year, m2_month, force=False
|
|||
m1_pool = monthly_students + bimonthly_m1
|
||||
m2_pool = monthly_students + bimonthly_m2
|
||||
|
||||
today = date.today()
|
||||
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
|
||||
|
||||
if not m1_exists or force:
|
||||
for idx, student in enumerate(m1_pool):
|
||||
monday_date = m1_mondays[idx % len(m1_mondays)]
|
||||
monday_date = valid_m1[idx % len(valid_m1)]
|
||||
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
||||
|
||||
if not m2_exists or force:
|
||||
for idx, student in enumerate(m2_pool):
|
||||
monday_date = m2_mondays[idx % len(m2_mondays)]
|
||||
monday_date = valid_m2[idx % len(valid_m2)]
|
||||
WeeklySchedule.objects.get_or_create(student=student, date=monday_date)
|
||||
|
||||
def index(request):
|
||||
|
|
@ -399,6 +407,7 @@ def record_attendance(request):
|
|||
students = Student.objects.filter(is_active=True).order_by('name')
|
||||
return render(request, 'students/record_attendance.html', {'students': students, 'point_records': point_records})
|
||||
|
||||
@login_required(login_url='login')
|
||||
def show_attendance(request, student_id):
|
||||
student = Student.objects.get(student_id=student_id)
|
||||
point_records = PointRecord.objects.filter(student=student).order_by('-datetime')
|
||||
|
|
@ -506,39 +515,41 @@ def weekly_schedule(request):
|
|||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
degree_order = Case(
|
||||
When(student__degree_type='博士生', then=Value(1)),
|
||||
When(student__degree_type='碩士生', then=Value(2)),
|
||||
When(student__degree_type='學士生', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
|
||||
schedule_m1 = []
|
||||
for m in m1_mondays:
|
||||
records = WeeklySchedule.objects.filter(date=m).select_related('student').annotate(
|
||||
cat_order=category_order
|
||||
).order_by('cat_order', 'student__student_id')
|
||||
cat_order=category_order,
|
||||
deg_order=degree_order,
|
||||
).order_by('cat_order', 'deg_order', 'student__student_id')
|
||||
schedule_m1.append({
|
||||
'date': m,
|
||||
'formatted': m.strftime('%Y-%m-%d'),
|
||||
'students': [r.student for r in records]
|
||||
'students': [r.student for r in records],
|
||||
'is_past': m < today,
|
||||
})
|
||||
|
||||
schedule_m2 = []
|
||||
for m in m2_mondays:
|
||||
records = WeeklySchedule.objects.filter(date=m).select_related('student').annotate(
|
||||
cat_order=category_order
|
||||
).order_by('cat_order', 'student__student_id')
|
||||
cat_order=category_order,
|
||||
deg_order=degree_order,
|
||||
).order_by('cat_order', 'deg_order', 'student__student_id')
|
||||
schedule_m2.append({
|
||||
'date': m,
|
||||
'formatted': m.strftime('%Y-%m-%d'),
|
||||
'students': [r.student for r in records]
|
||||
'students': [r.student for r in records],
|
||||
'is_past': m < today,
|
||||
})
|
||||
|
||||
student_cat_order = Case(
|
||||
When(student_category='一般生', then=Value(1)),
|
||||
When(student_category='在職生', then=Value(2)),
|
||||
When(student_category='交換中', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
active_students = Student.objects.filter(is_active=True).annotate(
|
||||
cat_order=student_cat_order
|
||||
).order_by('cat_order', 'student_id')
|
||||
active_students = Student.objects.filter(is_active=True).order_by('student_id')
|
||||
summary_data = []
|
||||
for s in active_students:
|
||||
assigned_m1 = [m['formatted'] for m in schedule_m1 if s in m['students']]
|
||||
|
|
@ -546,13 +557,15 @@ def weekly_schedule(request):
|
|||
assigned_dates = assigned_m1 + assigned_m2
|
||||
|
||||
if s.student_category == '交換中':
|
||||
rule_desc = '交換生 (免報告)'
|
||||
rule_desc = _('交換生 (免報告)')
|
||||
elif s.degree_type == '學士生':
|
||||
rule_desc = _('學士生 (1次/月)')
|
||||
elif s.degree_type == '碩士生':
|
||||
rule_desc = '碩士生 (1次/月)'
|
||||
rule_desc = _('碩士生 (1次/月)')
|
||||
elif s.degree_type == '博士生' and s.student_category == '一般生':
|
||||
rule_desc = '博士一般生 (1次/月)'
|
||||
rule_desc = _('博士一般生 (1次/月)')
|
||||
else:
|
||||
rule_desc = '博士在職生 (1次/2個月)'
|
||||
rule_desc = _('博士在職生 (1次/2個月)')
|
||||
|
||||
summary_data.append({
|
||||
'student': s,
|
||||
|
|
@ -565,6 +578,9 @@ def weekly_schedule(request):
|
|||
request.user.is_superuser or check_user_has_all_ad_groups(request.user, request, ['Administrators'])
|
||||
)
|
||||
|
||||
# Generate clean English static HTML for iframe embedding
|
||||
generate_english_schedule_static_html()
|
||||
|
||||
return render(request, 'students/weekly_schedule.html', {
|
||||
'm1_year': m1_year,
|
||||
'm1_month': m1_month,
|
||||
|
|
@ -616,7 +632,7 @@ def move_schedule_item(request):
|
|||
if student.student_category == '交換中':
|
||||
return JsonResponse({'success': False, 'error': '交換生無需報告,無法排程'}, status=400)
|
||||
|
||||
is_monthly = (student.degree_type == '碩士生') 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 from_date.month != to_date.month or from_date.year != to_date.year:
|
||||
|
|
@ -633,6 +649,9 @@ def move_schedule_item(request):
|
|||
schedule_entry.month = to_date.month
|
||||
schedule_entry.save()
|
||||
|
||||
# Update static English HTML for iframe embed
|
||||
generate_english_schedule_static_html()
|
||||
|
||||
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)
|
||||
schedule_logger.info(log_msg)
|
||||
|
|
@ -684,8 +703,12 @@ def verify_schedule_rules(request):
|
|||
|
||||
active_students = Student.objects.filter(is_active=True).exclude(student_category='交換中')
|
||||
|
||||
valid_m1_mondays = [d for d in m1_mondays if d >= today]
|
||||
valid_m2_mondays = [d for d in m2_mondays if d >= today]
|
||||
valid_all_mondays = [d for d in (m1_mondays + m2_mondays) if d >= today]
|
||||
|
||||
for s in active_students:
|
||||
is_monthly = (s.degree_type == '碩士生') or (s.degree_type == '博士生' and s.student_category == '一般生')
|
||||
is_monthly = (s.degree_type in ('碩士生', '學士生')) or (s.degree_type == '博士生' and s.student_category == '一般生')
|
||||
|
||||
if is_monthly:
|
||||
# Check Month 1
|
||||
|
|
@ -695,9 +718,10 @@ def verify_schedule_rules(request):
|
|||
r.delete()
|
||||
deleted_count += 1
|
||||
elif len(m1_records) == 0:
|
||||
monday = min(m1_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
created_count += 1
|
||||
if valid_m1_mondays:
|
||||
monday = min(valid_m1_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
created_count += 1
|
||||
|
||||
# Check Month 2
|
||||
m2_records = list(WeeklySchedule.objects.filter(student=s, year=m2_year, month=m2_month).order_by('date'))
|
||||
|
|
@ -706,9 +730,10 @@ def verify_schedule_rules(request):
|
|||
r.delete()
|
||||
deleted_count += 1
|
||||
elif len(m2_records) == 0:
|
||||
monday = min(m2_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
created_count += 1
|
||||
if valid_m2_mondays:
|
||||
monday = min(valid_m2_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
created_count += 1
|
||||
else:
|
||||
# 博士在職生: 1 report per 2 months
|
||||
bimonthly_records = list(WeeklySchedule.objects.filter(student=s, year__in=[m1_year, m2_year], month__in=[m1_month, m2_month]).order_by('date'))
|
||||
|
|
@ -717,16 +742,19 @@ def verify_schedule_rules(request):
|
|||
r.delete()
|
||||
deleted_count += 1
|
||||
elif len(bimonthly_records) == 0:
|
||||
all_mondays = m1_mondays + m2_mondays
|
||||
monday = min(all_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
created_count += 1
|
||||
if valid_all_mondays:
|
||||
monday = min(valid_all_mondays, key=lambda d: WeeklySchedule.objects.filter(date=d).count())
|
||||
WeeklySchedule.objects.create(student=s, date=monday)
|
||||
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."
|
||||
logger.info(log_msg)
|
||||
schedule_logger.info(log_msg)
|
||||
print(log_msg)
|
||||
|
||||
# Update static English HTML for iframe embed
|
||||
generate_english_schedule_static_html()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"校正與驗證排程規則完成!已清理 {deleted_count} 筆不符規則之卡片(包含刪除交換生卡片與重複排程),補齊 {created_count} 筆缺漏排程。"
|
||||
|
|
@ -821,3 +849,134 @@ def upload_students_excel(request):
|
|||
messages.error(request, f"上傳匯入學生資料失敗: {e}")
|
||||
|
||||
return redirect('student_list')
|
||||
|
||||
|
||||
def generate_english_schedule_static_html():
|
||||
"""
|
||||
Generates a standalone, clean English static HTML file for the weekly schedule,
|
||||
suitable for embedding directly in an <iframe>.
|
||||
Saves the output to static directories.
|
||||
"""
|
||||
today = date.today()
|
||||
m1_year, m1_month = today.year, today.month
|
||||
m2_date = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
|
||||
m2_year, m2_month = m2_date.year, m2_date.month
|
||||
|
||||
m1_mondays = get_month_mondays(m1_year, m1_month)
|
||||
m2_mondays = get_month_mondays(m2_year, m2_month)
|
||||
|
||||
category_order = Case(
|
||||
When(student__student_category='一般生', then=Value(1)),
|
||||
When(student__student_category='在職生', then=Value(2)),
|
||||
When(student__student_category='交換中', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
degree_order = Case(
|
||||
When(student__degree_type='博士生', then=Value(1)),
|
||||
When(student__degree_type='碩士生', then=Value(2)),
|
||||
When(student__degree_type='學士生', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
|
||||
schedule_m1 = []
|
||||
for m in m1_mondays:
|
||||
records = WeeklySchedule.objects.filter(date=m).select_related('student').annotate(
|
||||
cat_order=category_order,
|
||||
deg_order=degree_order,
|
||||
).order_by('cat_order', 'deg_order', 'student__student_id')
|
||||
schedule_m1.append({
|
||||
'date': m,
|
||||
'formatted': m.strftime('%Y-%m-%d'),
|
||||
'students': [r.student for r in records],
|
||||
'is_past': m < today,
|
||||
})
|
||||
|
||||
schedule_m2 = []
|
||||
for m in m2_mondays:
|
||||
records = WeeklySchedule.objects.filter(date=m).select_related('student').annotate(
|
||||
cat_order=category_order,
|
||||
deg_order=degree_order,
|
||||
).order_by('cat_order', 'deg_order', 'student__student_id')
|
||||
schedule_m2.append({
|
||||
'date': m,
|
||||
'formatted': m.strftime('%Y-%m-%d'),
|
||||
'students': [r.student for r in records],
|
||||
'is_past': m < today,
|
||||
})
|
||||
|
||||
student_cat_order = Case(
|
||||
When(student_category='一般生', then=Value(1)),
|
||||
When(student_category='在職生', then=Value(2)),
|
||||
When(student_category='交換中', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
student_deg_order = Case(
|
||||
When(degree_type='博士生', then=Value(1)),
|
||||
When(degree_type='碩士生', then=Value(2)),
|
||||
When(degree_type='學士生', then=Value(3)),
|
||||
default=Value(4),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
active_students = Student.objects.filter(is_active=True).annotate(
|
||||
cat_order=student_cat_order,
|
||||
deg_order=student_deg_order,
|
||||
).order_by('cat_order', 'deg_order', 'student_id')
|
||||
summary_data = []
|
||||
for s in active_students:
|
||||
assigned_m1 = [m['formatted'] for m in schedule_m1 if s in m['students']]
|
||||
assigned_m2 = [m['formatted'] for m in schedule_m2 if s in m['students']]
|
||||
assigned_dates = assigned_m1 + assigned_m2
|
||||
|
||||
if s.student_category == '交換中':
|
||||
rule_desc = 'Exchange Student (Exempt)'
|
||||
elif s.degree_type == '學士生':
|
||||
rule_desc = 'Undergraduate Student (1 time/month)'
|
||||
elif s.degree_type == '碩士生':
|
||||
rule_desc = 'Master Student (1 time/month)'
|
||||
elif s.degree_type == '博士生' and s.student_category == '一般生':
|
||||
rule_desc = 'Ph.D. Regular (1 time/month)'
|
||||
else:
|
||||
rule_desc = 'Ph.D. In-Service (1 time/2 months)'
|
||||
|
||||
summary_data.append({
|
||||
'student': s,
|
||||
'rule_desc': rule_desc,
|
||||
'assigned_dates': assigned_dates,
|
||||
'total_count': len(assigned_dates)
|
||||
})
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
html_content = render_to_string('students/weekly_schedule_en_iframe.html', {
|
||||
'm1_year': m1_year,
|
||||
'm1_month': m1_month,
|
||||
'm2_year': m2_year,
|
||||
'm2_month': m2_month,
|
||||
'schedule_m1': schedule_m1,
|
||||
'schedule_m2': schedule_m2,
|
||||
'summary_data': summary_data,
|
||||
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
})
|
||||
|
||||
import os
|
||||
target_paths = [
|
||||
settings.BASE_DIR / 'static' / 'weekly_schedule_en.html',
|
||||
settings.BASE_DIR / 'students' / 'static' / 'students' / 'weekly_schedule_en.html',
|
||||
]
|
||||
for path in target_paths:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
return html_content
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def weekly_schedule_en_iframe(request):
|
||||
"""
|
||||
Serves the clean English static HTML schedule for iframe embedding.
|
||||
"""
|
||||
html_content = generate_english_schedule_static_html()
|
||||
return HttpResponse(html_content, content_type='text/html; charset=utf-8')
|
||||
Loading…
Reference in a new issue