From 7b5abd244f6af9e107c4693d117d7fe7626979a9 Mon Sep 17 00:00:00 2001 From: Xiao Furen Date: Tue, 4 Aug 2026 06:06:44 +0800 Subject: [PATCH] feat: implement student attendance tracking and weekly report scheduling system with AD authentication support --- .gitignore | 415 +++++----- point_system/locale/en/LC_MESSAGES/django.po | 172 ++++ .../locale/zh_Hant/LC_MESSAGES/django.po | 37 + point_system/point_system/settings.py | 63 +- point_system/students.csv | 31 +- point_system/students/ad_backend.py | 185 +++++ point_system/students/admin.py | 29 +- point_system/students/context_processors.py | 9 + point_system/students/forms.py | 13 + .../management/commands/test_ad_auth.py | 25 + .../migrations/0002_student_degree_type.py | 18 + .../0003_student_student_category.py | 18 + .../0004_alter_student_student_category.py | 18 + .../migrations/0005_weeklyschedule.py | 30 + ...chedule_options_student_email2_and_more.py | 37 + point_system/students/models.py | 38 +- point_system/students/templates/base.html | 137 ++- .../students/templates/students/login.html | 51 ++ .../templates/students/show_attendance.html | 212 +++++ .../templates/students/student_list.html | 232 +++++- .../templates/students/user_groups.html | 83 ++ .../templates/students/weekly_schedule.html | 351 ++++++++ point_system/students/tests.py | 556 ++++++++++++- point_system/students/urls.py | 13 +- point_system/students/views.py | 780 +++++++++++++++++- 25 files changed, 3231 insertions(+), 322 deletions(-) create mode 100644 point_system/locale/en/LC_MESSAGES/django.po create mode 100644 point_system/locale/zh_Hant/LC_MESSAGES/django.po create mode 100644 point_system/students/ad_backend.py create mode 100644 point_system/students/context_processors.py create mode 100644 point_system/students/management/commands/test_ad_auth.py create mode 100644 point_system/students/migrations/0002_student_degree_type.py create mode 100644 point_system/students/migrations/0003_student_student_category.py create mode 100644 point_system/students/migrations/0004_alter_student_student_category.py create mode 100644 point_system/students/migrations/0005_weeklyschedule.py create mode 100644 point_system/students/migrations/0006_alter_weeklyschedule_options_student_email2_and_more.py create mode 100644 point_system/students/templates/students/login.html create mode 100644 point_system/students/templates/students/show_attendance.html create mode 100644 point_system/students/templates/students/user_groups.html create mode 100644 point_system/students/templates/students/weekly_schedule.html diff --git a/.gitignore b/.gitignore index 78d3117..23bc5b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,206 +1,209 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml - -######### -db.sqlite3.* \ No newline at end of file +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +######### +db.sqlite3.* +*.bak +*.csv +*.xlsx diff --git a/point_system/locale/en/LC_MESSAGES/django.po b/point_system/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..ffad73c --- /dev/null +++ b/point_system/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,172 @@ +msgid "" +msgstr "" +"Project-Id-Version: 1218 Lab 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-04 05:20+0800\n" +"PO-Revision-Date: 2026-08-04 05:20+0800\n" +"Last-Translator: 1218 Lab\n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "首頁" +msgstr "Home" + +msgid "記錄考勤" +msgstr "Record Attendance" + +msgid "最近考勤" +msgstr "Recent Attendance" + +msgid "考勤報告" +msgstr "Attendance Report" + +msgid "學生列表" +msgstr "Student List" + +msgid "週會排程" +msgstr "Weekly Schedule" + +msgid "登出" +msgstr "Logout" + +msgid "AD 登入" +msgstr "AD Login" + +msgid "僅顯示在學學生 (預設)" +msgstr "Active Students Only (Default)" + +msgid "全部學生" +msgstr "All Students" + +msgid "僅顯示在學學生" +msgstr "Active Students Only" + +msgid "上傳並匯入 students.xlsx" +msgstr "Upload & Import students.xlsx" + +msgid "學號" +msgstr "Student ID" + +msgid "姓名" +msgstr "Name" + +msgid "在學狀態" +msgstr "Status" + +msgid "學位類別" +msgstr "Degree Type" + +msgid "身分類別" +msgstr "Category" + +msgid "班級/年級" +msgstr "Grade" + +msgid "詳細資料" +msgstr "Details" + +msgid "查看詳情" +msgstr "View Details" + +msgid "在學" +msgstr "Active" + +msgid "非在學" +msgstr "Inactive" + +msgid "碩士生" +msgstr "Master Student" + +msgid "博士生" +msgstr "Ph.D. Student" + +msgid "一般生" +msgstr "Regular" + +msgid "在職生" +msgstr "In-Service" + +msgid "交換中" +msgstr "Exchange" + +msgid "碩士生 (一般 & 在職)" +msgstr "Master Student (Regular & In-Service)" + +msgid "博士一般生" +msgstr "Ph.D. Regular Student" + +msgid "博士在職生" +msgstr "Ph.D. In-Service Student" + +msgid "交換生 (交換中)" +msgstr "Exchange Student (On Exchange)" + +msgid "排程頻率: 每月 1 次" +msgstr "Schedule Frequency: 1 time / month" + +msgid "排程頻率: 每 2 個月 1 次" +msgstr "Schedule Frequency: 1 time / 2 months" + +msgid "排程狀態: 免報告" +msgstr "Schedule Status: Exempt" + +msgid "碩士生 (1次/月)" +msgstr "Master Student (1 time/month)" + +msgid "博士一般生 (1次/月)" +msgstr "Ph.D. Regular (1 time/month)" + +msgid "博士在職生 (1次/2個月)" +msgstr "Ph.D. In-Service (1 time/2 months)" + +msgid "交換生 (免報告)" +msgstr "Exchange Student (Exempt)" + +msgid "週會報告排程規則" +msgstr "Weekly Seminar Schedule Rules" + +msgid "校正並驗證排程規則" +msgstr "Verify & Clean Schedule Rules" + +msgid "資料庫狀態: 已同步儲存" +msgstr "DB Status: Synced" + +msgid "位報告者" +msgstr "Presenters" + +msgid "當日無排定報告者" +msgstr "No presenters scheduled for this day" + +msgid "學生個人報告統計明細 (兩月排程核對)" +msgstr "Student Seminar Presentation Statistics" + +msgid "身份 / 規則" +msgstr "Category / Rule" + +msgid "排定報告日期 (Mondays)" +msgstr "Scheduled Dates (Mondays)" + +msgid "應報告次數" +msgstr "Total Reports Required" + +msgid "免報告" +msgstr "Exempt" + +msgid "次" +msgstr "time(s)" + +msgid "拖曳排程微調提示:" +msgstr "Drag & Drop Tip: " + +msgid "您已登入系統,可將學生列直接拖曳 (Drag & Drop) 至其他週一日期卡片中調整報告時間。系統將會在寫入資料庫前自動驗證報告頻率規則,並詳細記錄您的變更日誌。" +msgstr "You are logged in. You can drag and drop student cards directly to other Monday date cards to adjust presentation dates. The system will automatically verify schedule frequency rules before saving to the database and log all changes." + +msgid "檢視模式:" +msgstr "View-Only Mode: " + +msgid "登入系統後,可拖曳學生調整報告日期。" +msgstr "Log in to drag and drop student cards to adjust presentation dates." diff --git a/point_system/locale/zh_Hant/LC_MESSAGES/django.po b/point_system/locale/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 0000000..595c2f6 --- /dev/null +++ b/point_system/locale/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,37 @@ +msgid "" +msgstr "" +"Project-Id-Version: 1218 Lab 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-04 05:20+0800\n" +"PO-Revision-Date: 2026-08-04 05:20+0800\n" +"Last-Translator: 1218 Lab\n" +"Language-Team: Traditional Chinese\n" +"Language: zh_Hant\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "首頁" +msgstr "首頁" + +msgid "記錄考勤" +msgstr "記錄考勤" + +msgid "最近考勤" +msgstr "最近考勤" + +msgid "考勤報告" +msgstr "考勤報告" + +msgid "學生列表" +msgstr "學生列表" + +msgid "週會排程" +msgstr "週會排程" + +msgid "登出" +msgstr "登出" + +msgid "AD 登入" +msgstr "AD 登入" diff --git a/point_system/point_system/settings.py b/point_system/point_system/settings.py index c7d9068..50e3993 100644 --- a/point_system/point_system/settings.py +++ b/point_system/point_system/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -65,6 +66,8 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.i18n', + 'students.context_processors.language_processor', ], }, }, @@ -102,20 +105,37 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# Active Directory Authentication Settings +AD_SERVER = 'dc1.samdom.mdi.bar' +AD_DOMAIN = 'samdom.mdi.bar' +AD_BASE_DN = 'dc=samdom,dc=mdi,dc=bar' + +AUTHENTICATION_BACKENDS = [ + 'students.ad_backend.ActiveDirectoryBackend', + 'django.contrib.auth.backends.ModelBackend', +] + + # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'zh-hant' - -TIME_ZONE = 'UTC' TIME_ZONE = 'Asia/Taipei' - USE_I18N = True - USE_TZ = True +from django.utils.translation import gettext_lazy as _ + +LANGUAGES = [ + ('zh-hant', _('Traditional Chinese')), + ('en', _('English')), +] + +LOCALE_PATHS = [ + BASE_DIR / 'locale', +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ @@ -126,3 +146,36 @@ STATIC_URL = 'static/' # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# File Logging Configuration for Schedule Actions +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{asctime} [{levelname}] {message}', + 'style': '{', + }, + }, + 'handlers': { + 'schedule_file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'schedule_actions.log', + 'formatter': 'verbose', + 'encoding': 'utf-8', + }, + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'schedule_logger': { + 'handlers': ['schedule_file', 'console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} diff --git a/point_system/students.csv b/point_system/students.csv index a1a502d..76bf788 100644 --- a/point_system/students.csv +++ b/point_system/students.csv @@ -11,15 +11,22 @@ Financial assistances for disadvantaged students","就學貸款 Loan",Email -D13K48007,李艾臻,1,'-,'-,'-,0,D13K48007@ntu.edu.tw;zaq4747@yahoo.com.tw -R13458003,林慕峰,1,'-,'-,'-,0,R13458003@ntu.edu.tw;brenton.lin.tw@gmail.com -R13458019,"林 樸",1,'-,'-,'-,0,R13458019@ntu.edu.tw;pu@johnsonandannie.com -R13K47020,劉書瑜,1,'-,'-,'-,0,R13K47020@ntu.edu.tw;freestyle770614@gmail.com -R13K47029,陳品辰,1,'-,'-,'-,0,R13K47029@ntu.edu.tw;sw1sw2sw3@gmail.com -R13K47031,"NANDHITHA SURULIANDI",1,'-,'-,'-,0,R13K47031@ntu.edu.tw;nandhithasuruliandi@gmail.com -D12K48020,傅冠豪,2,'-,'-,'-,0,D12K48020@ntu.edu.tw;haobbc@hotmail.com -R12458006,周宜葇,2,'-,通過,'-,0,R12458006@ntu.edu.tw;r12458006@g.ntu.edu.tw -R12458007,林育暄,2,'-,'-,'-,0,R12458007@ntu.edu.tw;sunny891228@gmail.com -R12458014,劉羽軒,2,'-,'-,'-,0,R12458014@ntu.edu.tw;liumilk613@gmail.com -R12458015,"ZOLNAMAR DORJSEMBE",2,'-,'-,'-,0,R12458015@ntu.edu.tw;zolnamar@gmail.com -R11458015,郭士榮,3,'-,'-,'-,0,R11458015@ntu.edu.tw;tonypaul57@gmail.com +D12458003,林孟忻,3,'-,'-,'-,0,D12458003@ntu.edu.tw;glingloria@gmail.com +D12K48020,傅冠豪,3,'-,'-,'-,0,D12K48020@ntu.edu.tw;haobbc@hotmail.com +D13K48007,李艾臻,2,'-,'-,'-,0,D13K48007@ntu.edu.tw;zaq4747@yahoo.com.tw +D13K48008,林冠儒,2,'-,'-,'-,0,D13K48008@ntu.edu.tw;atmoonsamo@gmail.com +D14458011,吳瓊芳,1,'-,'-,'-,0,D14458011@ntu.edu.tw;chfu.wu@gmail.com +D14K48002,黃子芬,1,'-,'-,'-,0,D14K48002@ntu.edu.tw;18390@s.tmu.edu.tw +D14K48007,陳其蒙,1,'-,'-,'-,0,D14K48007@ntu.edu.tw;kimochen53@gmail.com +D14K48010,"BUSHRA UROOJ",1,'-,'-,'-,0,D14K48010@ntu.edu.tw;bushrauroojluckygirl@gmil.com +R11458015,郭士榮,4,'-,'-,'-,0,R11458015@ntu.edu.tw;tonypaul57@gmail.com +R12458006,周宜葇,3,'-,'-,'-,0,R12458006@ntu.edu.tw;r12458006@g.ntu.edu.tw +R13458003,林慕峰,2,'-,'-,'-,0,R13458003@ntu.edu.tw;brenton.lin.tw@gmail.com +R13458019,"林 樸",2,'-,'-,'-,0,R13458019@ntu.edu.tw;pu@johnsonandannie.com +R13K47020,劉書瑜,2,'-,'-,'-,0,R13K47020@ntu.edu.tw;freestyle770614@gmail.com +R13K47029,陳品辰,2,'-,'-,'-,0,R13K47029@ntu.edu.tw;sw1sw2sw3@gmail.com +R13K47031,"NANDHITHA SURULIANDI",2,'-,'-,'-,0,R13K47031@ntu.edu.tw;nandhithasuruliandi@gmail.com +R14458006,林靖哲,1,'-,'-,'-,0,R14458006@ntu.edu.tw;jimmy910225@gmail.com +R14K47018,李亮慶,1,'-,'-,'-,0,R14K47018@ntu.edu.tw;liangching0926@gmail.com +R14K47019,劉冠岑,1,'-,'-,'-,0,R14K47019@ntu.edu.tw;a0938283665@gmail.com +R14K47038,林綺音,1,'-,'-,'-,0,R14K47038@ntu.edu.tw;erinyes21828357@gmail.com diff --git a/point_system/students/ad_backend.py b/point_system/students/ad_backend.py new file mode 100644 index 0000000..29390e8 --- /dev/null +++ b/point_system/students/ad_backend.py @@ -0,0 +1,185 @@ +import logging +import ldap3 +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import BaseBackend + +logger = logging.getLogger(__name__) +User = get_user_model() + +def parse_group_dn(dn): + """Extract Common Name (CN) of group and Organizational Unit (OU) path from DN.""" + cn = None + parts = dn.split(',') + ou_list = [] + for part in parts: + part_str = part.strip() + if part_str.upper().startswith('CN='): + if cn is None: + cn = part_str[3:] + elif part_str.upper().startswith('OU='): + ou_list.append(part_str[3:]) + + if cn is None: + cn = dn + ou_path = " -> ".join(reversed(ou_list)) if ou_list else "Domain Root" + return { + 'cn': cn, + 'dn': dn, + 'ou_path': ou_path + } + +def fetch_ad_groups(username, password=None): + """ + Fetch Active Directory groups for a given user from dc1.samdom.mdi.bar. + """ + ad_server = getattr(settings, 'AD_SERVER', 'dc1.samdom.mdi.bar') + ad_domain = getattr(settings, 'AD_DOMAIN', 'samdom.mdi.bar') + ad_base_dn = getattr(settings, 'AD_BASE_DN', 'dc=samdom,dc=mdi,dc=bar') + + short_username = username.split('@')[0].split('\\')[-1] if username else username + user_principal = f"{short_username}@{ad_domain}" if '@' not in username and '\\' not in username else username + + groups = [] + try: + server = ldap3.Server(ad_server, get_info=ldap3.NONE, connect_timeout=5) + if password: + conn = ldap3.Connection(server, user=user_principal, password=password, authentication=ldap3.SIMPLE) + bound = conn.bind() + else: + conn = ldap3.Connection(server, auto_bind=True) + bound = True + + if bound: + search_filter = f"(sAMAccountName={short_username})" + conn.search( + search_base=ad_base_dn, + search_filter=search_filter, + attributes=['memberOf', 'cn', 'displayName', 'distinguishedName'] + ) + if conn.entries: + entry = conn.entries[0] + if hasattr(entry, 'memberOf') and entry.memberOf.values: + for group_dn in entry.memberOf.values: + groups.append(parse_group_dn(group_dn)) + + conn.unbind() + except Exception as e: + logger.error(f"Error fetching AD groups for {username}: {e}") + + return groups + + +class ActiveDirectoryBackend(BaseBackend): + """ + Authentication backend for Active Directory (DC: dc1.samdom.mdi.bar). + Authenticates users against Active Directory via LDAP/AD simple bind. + """ + def __init__(self): + self.ad_server = getattr(settings, 'AD_SERVER', 'dc1.samdom.mdi.bar') + self.ad_domain = getattr(settings, 'AD_DOMAIN', 'samdom.mdi.bar') + self.ad_base_dn = getattr(settings, 'AD_BASE_DN', 'dc=samdom,dc=mdi,dc=bar') + + def authenticate(self, request, username=None, password=None, **kwargs): + if not username or not password: + return None + + username = username.strip() + if '@' in username: + user_principal = username + short_username = username.split('@')[0] + elif '\\' in username: + parts = username.split('\\') + short_username = parts[1] + user_principal = f"{short_username}@{self.ad_domain}" + else: + short_username = username + user_principal = f"{username}@{self.ad_domain}" + + try: + server = ldap3.Server(self.ad_server, get_info=ldap3.NONE, connect_timeout=5) + conn = ldap3.Connection( + server, + user=user_principal, + password=password, + authentication=ldap3.SIMPLE, + auto_bind=False + ) + + if not conn.bind(): + logger.warning(f"Active Directory authentication failed for {user_principal}: {conn.result}") + return None + + logger.info(f"Active Directory authentication succeeded for {user_principal}") + + email = f"{short_username}@{self.ad_domain}" + first_name = "" + last_name = "" + ad_groups = [] + + try: + search_filter = f"(sAMAccountName={short_username})" + conn.search( + search_base=self.ad_base_dn, + search_filter=search_filter, + attributes=['mail', 'givenName', 'sn', 'displayName', 'memberOf'] + ) + if conn.entries: + entry = conn.entries[0] + if hasattr(entry, 'mail') and entry.mail.value: + email = entry.mail.value + if hasattr(entry, 'givenName') and entry.givenName.value: + first_name = entry.givenName.value + if hasattr(entry, 'sn') and entry.sn.value: + last_name = entry.sn.value + elif hasattr(entry, 'displayName') and entry.displayName.value: + first_name = entry.displayName.value + + if hasattr(entry, 'memberOf') and entry.memberOf.values: + for group_dn in entry.memberOf.values: + ad_groups.append(parse_group_dn(group_dn)) + except Exception as e: + logger.debug(f"Error querying extra user attributes from AD: {e}") + finally: + try: + conn.unbind() + except Exception: + pass + + user, created = User.objects.get_or_create( + username=short_username, + defaults={ + 'email': email, + 'first_name': first_name, + 'last_name': last_name, + 'is_staff': True, + 'is_active': True, + } + ) + + if not created: + updated = False + if email and user.email != email: + user.email = email + updated = True + if first_name and user.first_name != first_name: + user.first_name = first_name + updated = True + if last_name and user.last_name != last_name: + user.last_name = last_name + updated = True + if updated: + user.save() + + user.ad_groups = ad_groups + return user + + except Exception as e: + logger.error(f"Active Directory backend error: {e}") + return None + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None diff --git a/point_system/students/admin.py b/point_system/students/admin.py index e633f24..5c04d7c 100644 --- a/point_system/students/admin.py +++ b/point_system/students/admin.py @@ -1,26 +1,33 @@ from django.contrib import admin -from .models import Student, PointRecord +from .models import Student, PointRecord, WeeklySchedule @admin.register(Student) class StudentAdmin(admin.ModelAdmin): - list_display = ('student_id', 'name', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'email', 'is_active') - search_fields = ('student_id', 'name') - ordering = ['student_id'] # Changed to order by student_id as an example - list_filter = ['grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'is_active'] + list_display = ('student_id', 'name', 'degree_type', 'student_category', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'email', 'email2', 'is_active') + search_fields = ('student_id', 'name', 'email', 'email2') + ordering = ['student_id'] + list_filter = ['degree_type', 'student_category', 'grade', 'hope_admission_scheme', 'tuition_waiver', 'financial_assistance_disadvantaged_students', 'loan', 'is_active'] @admin.register(PointRecord) class PointRecordAdmin(admin.ModelAdmin): - list_display = ('student', 'datetime') # Updated to use datetime + list_display = ('student', 'datetime') search_fields = ['student__name', 'student__student_id'] - list_filter = ['datetime'] # Updated to use datetime - ordering = ['-datetime'] # Updated to use datetime + list_filter = ['datetime'] + ordering = ['-datetime'] fieldsets = [ (None, { 'fields': ['student'] }), ('點名時間', { - 'fields': ['datetime'], # Updated to use datetime - 'classes': ['collapse'] # 顯示為可展開的區塊 + 'fields': ['datetime'], + 'classes': ['collapse'] }), - ] \ No newline at end of file + ] + +@admin.register(WeeklySchedule) +class WeeklyScheduleAdmin(admin.ModelAdmin): + list_display = ('date', 'student', 'year', 'month') + search_fields = ['student__name', 'student__student_id'] + list_filter = ['year', 'month', 'date'] + ordering = ['date', 'student__student_id'] \ No newline at end of file diff --git a/point_system/students/context_processors.py b/point_system/students/context_processors.py new file mode 100644 index 0000000..e44f670 --- /dev/null +++ b/point_system/students/context_processors.py @@ -0,0 +1,9 @@ +def language_processor(request): + """ + Context processor to make current_lang available across all template renders. + Defaults to 'zh' (Traditional Chinese). + """ + lang = request.session.get('lang', 'zh') + return { + 'current_lang': lang + } diff --git a/point_system/students/forms.py b/point_system/students/forms.py index de4b8e6..adb8db4 100644 --- a/point_system/students/forms.py +++ b/point_system/students/forms.py @@ -5,4 +5,17 @@ class StudentLoginForm(forms.Form): label='學號或姓名', max_length=100, required=True + ) + +class ADLoginForm(forms.Form): + username = forms.CharField( + label='AD 帳號', + max_length=150, + required=True, + widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '帳號 (例如: user 或 user@samdom.mdi.bar)'}) + ) + password = forms.CharField( + label='密碼', + required=True, + widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '密碼'}) ) \ No newline at end of file diff --git a/point_system/students/management/commands/test_ad_auth.py b/point_system/students/management/commands/test_ad_auth.py new file mode 100644 index 0000000..6fc3e78 --- /dev/null +++ b/point_system/students/management/commands/test_ad_auth.py @@ -0,0 +1,25 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import authenticate + +class Command(BaseCommand): + help = 'Test Active Directory authentication against dc1.samdom.mdi.bar' + + def add_arguments(self, parser): + parser.add_argument('username', type=str, help='Active Directory username') + parser.add_argument('password', type=str, help='Active Directory password') + + def handle(self, *args, **options): + username = options['username'] + password = options['password'] + + self.stdout.write(f"Attempting AD authentication for user '{username}' on dc1.samdom.mdi.bar...") + user = authenticate(username=username, password=password) + + if user is not None: + self.stdout.write(self.style.SUCCESS( + f"SUCCESS: Authenticated user '{user.username}' (Email: '{user.email}', Name: '{user.get_full_name()}')" + )) + else: + self.stdout.write(self.style.ERROR( + f"FAILURE: Authentication failed for user '{username}'." + )) diff --git a/point_system/students/migrations/0002_student_degree_type.py b/point_system/students/migrations/0002_student_degree_type.py new file mode 100644 index 0000000..5b558ab --- /dev/null +++ b/point_system/students/migrations/0002_student_degree_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2026-08-03 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='degree_type', + field=models.CharField(choices=[('碩士生', '碩士生'), ('博士生', '博士生')], default='碩士生', max_length=20, verbose_name='學位類別'), + ), + ] diff --git a/point_system/students/migrations/0003_student_student_category.py b/point_system/students/migrations/0003_student_student_category.py new file mode 100644 index 0000000..de10761 --- /dev/null +++ b/point_system/students/migrations/0003_student_student_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2026-08-03 15:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0002_student_degree_type'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='student_category', + field=models.CharField(choices=[('一般生', '一般生'), ('在職生', '在職生')], default='一般生', max_length=20, verbose_name='身分類別'), + ), + ] diff --git a/point_system/students/migrations/0004_alter_student_student_category.py b/point_system/students/migrations/0004_alter_student_student_category.py new file mode 100644 index 0000000..9e665bb --- /dev/null +++ b/point_system/students/migrations/0004_alter_student_student_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2026-08-03 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0003_student_student_category'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='student_category', + field=models.CharField(choices=[('一般生', '一般生'), ('在職生', '在職生'), ('交換中', '交換中')], default='一般生', max_length=20, verbose_name='身分類別'), + ), + ] diff --git a/point_system/students/migrations/0005_weeklyschedule.py b/point_system/students/migrations/0005_weeklyschedule.py new file mode 100644 index 0000000..a36ef16 --- /dev/null +++ b/point_system/students/migrations/0005_weeklyschedule.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2 on 2026-08-03 16:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0004_alter_student_student_category'), + ] + + operations = [ + migrations.CreateModel( + name='WeeklySchedule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='週會日期')), + ('year', models.IntegerField(verbose_name='年份')), + ('month', models.IntegerField(verbose_name='月份')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='students.student')), + ], + options={ + 'verbose_name': '週會排程', + 'verbose_name_plural': '週會排程', + 'ordering': ['date', 'student__student_id'], + 'unique_together': {('student', 'date')}, + }, + ), + ] diff --git a/point_system/students/migrations/0006_alter_weeklyschedule_options_student_email2_and_more.py b/point_system/students/migrations/0006_alter_weeklyschedule_options_student_email2_and_more.py new file mode 100644 index 0000000..1bbac09 --- /dev/null +++ b/point_system/students/migrations/0006_alter_weeklyschedule_options_student_email2_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2 on 2026-08-03 21:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0005_weeklyschedule'), + ] + + operations = [ + migrations.AlterModelOptions( + name='weeklyschedule', + options={'verbose_name': '週會排程', 'verbose_name_plural': '週會排程'}, + ), + migrations.AddField( + model_name='student', + name='email2', + field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='第二 Email'), + ), + migrations.AlterField( + model_name='student', + name='degree_type', + field=models.CharField(choices=[('碩士生', '碩士生'), ('博士生', '博士生')], default='碩士生', max_length=10, verbose_name='學位類別'), + ), + migrations.AlterField( + model_name='student', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='主要 Email'), + ), + migrations.AlterField( + model_name='student', + name='student_category', + field=models.CharField(choices=[('一般生', '一般生'), ('在職生', '在職生'), ('交換中', '交換中')], default='一般生', max_length=10, verbose_name='身分類別'), + ), + ] diff --git a/point_system/students/models.py b/point_system/students/models.py index 125e978..b8a5743 100644 --- a/point_system/students/models.py +++ b/point_system/students/models.py @@ -1,18 +1,32 @@ from django.db import models +DEGREE_CHOICES = [ + ('碩士生', '碩士生'), + ('博士生', '博士生'), +] + +CATEGORY_CHOICES = [ + ('一般生', '一般生'), + ('在職生', '在職生'), + ('交換中', '交換中'), +] + class Student(models.Model): student_id = models.CharField(max_length=10, primary_key=True) name = models.CharField(max_length=255) + 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='身分類別') grade = models.IntegerField(null=True, blank=True) hope_admission_scheme = models.BooleanField(default=False, null=True, blank=True) tuition_waiver = models.BooleanField(default=False, null=True, blank=True) financial_assistance_disadvantaged_students = models.BooleanField(default=False, null=True, blank=True) loan = models.IntegerField(null=True, blank=True, default=0) - email = models.EmailField(null=True, blank=True) + email = 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) def __str__(self): - return self.name + return f"{self.name} ({self.degree_type} / {self.student_category})" class PointRecord(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) @@ -24,3 +38,23 @@ class PointRecord(models.Model): def __str__(self): return f"{self.datetime} - {self.student}" + +class WeeklySchedule(models.Model): + student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='schedules') + date = models.DateField(verbose_name='週會日期') + year = models.IntegerField(verbose_name='年份') + month = models.IntegerField(verbose_name='月份') + + class Meta: + unique_together = ('student', 'date') + verbose_name = '週會排程' + verbose_name_plural = '週會排程' + + def save(self, *args, **kwargs): + if self.date: + self.year = self.date.year + self.month = self.date.month + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.date} - {self.student.name} ({self.student.student_id})" diff --git a/point_system/students/templates/base.html b/point_system/students/templates/base.html index 972be29..6aa9538 100644 --- a/point_system/students/templates/base.html +++ b/point_system/students/templates/base.html @@ -1,19 +1,22 @@ +{% load i18n %} - + + - {% block title %}學生考勤系統{% endblock %} + {% block title %}1218 Lab{% endblock %} + - -