From cd502df19aa809b0d3d7051f9ac00ac9df8bdd32 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Thu, 30 Jun 2022 15:12:40 -0600 Subject: [PATCH] Initial commit --- .gitignore | 130 +++++ Pipfile | 16 + Pipfile.lock | 135 +++++ generate_templates.py | 63 +++ readme.md | 25 + requirements.txt | 69 +++ setup.yaml | 17 + src/accounts/__init__.py | 0 src/accounts/admin.py | 5 + src/accounts/apps.py | 6 + src/accounts/forms.py | 13 + src/accounts/migrations/0001_initial.py | 45 ++ src/accounts/migrations/__init__.py | 0 src/accounts/models.py | 27 + .../templates/accounts/account_create.html | 14 + .../templates/accounts/account_detail.html | 72 +++ .../templates/accounts/account_form.html | 18 + .../templates/accounts/account_list.html | 23 + src/accounts/templates/accounts/profile.html | 72 +++ .../templates/accounts/profile_form.html | 10 + src/accounts/tests.py | 3 + src/accounts/urls.py | 11 + src/accounts/views.py | 75 +++ src/attendance/__init__.py | 0 src/attendance/admin.py | 6 + src/attendance/apps.py | 6 + src/attendance/forms.py | 24 + src/attendance/migrations/0001_initial.py | 39 ++ src/attendance/migrations/__init__.py | 0 src/attendance/models.py | 33 ++ .../attendance/day_confirm_delete.html | 13 + .../templates/attendance/day_create_form.html | 34 ++ .../templates/attendance/day_form.html | 34 ++ .../templates/attendance/day_list.html | 51 ++ .../attendance/entry_confirm_delete.html | 13 + .../templates/attendance/entry_form.html | 20 + src/attendance/tests.py | 3 + src/attendance/urls.py | 20 + src/attendance/views.py | 98 ++++ src/core/__init__.py | 0 src/core/admin.py | 3 + src/core/apps.py | 6 + src/core/forms.py | 37 ++ src/core/migrations/0001_initial.py | 114 +++++ .../0002_alter_schoolyear_start_date.py | 18 + ...003_remove_schoolyear_end_date_and_more.py | 27 + .../migrations/0004_alter_schoolyear_year.py | 18 + ...005_attendanceentry_created_at_and_more.py | 104 ++++ src/core/migrations/0006_score.py | 26 + src/core/migrations/__init__.py | 0 src/core/models.py | 259 ++++++++++ .../templates/core/partials/pagination.html | 17 + .../core/schoolyear_confirm_delete.html | 13 + .../core/schoolyear_create_form.html | 12 + .../templates/core/schoolyear_detail.html | 34 ++ src/core/templates/core/schoolyear_form.html | 12 + src/core/templates/core/schoolyear_list.html | 46 ++ .../core/student_confirm_delete.html | 13 + .../templates/core/student_create_form.html | 24 + src/core/templates/core/student_detail.html | 131 +++++ src/core/templates/core/student_form.html | 12 + src/core/templates/core/student_list.html | 47 ++ src/core/templatetags/__init__.py | 0 src/core/templatetags/helpers.py | 16 + src/core/tests.py | 3 + src/core/urls.py | 61 +++ src/core/views.py | 152 ++++++ src/gradebook/__init__.py | 0 src/gradebook/admin.py | 12 + src/gradebook/apps.py | 6 + src/gradebook/forms.py | 43 ++ src/gradebook/migrations/0001_initial.py | 69 +++ .../0002_component_finished_grading.py | 18 + src/gradebook/migrations/__init__.py | 0 src/gradebook/models.py | 100 ++++ .../gradebook/component_confirm_delete.html | 13 + .../gradebook/component_create_form.html | 17 + .../templates/gradebook/component_detail.html | 77 +++ .../templates/gradebook/component_form.html | 19 + .../gradebook/component_manager.html | 41 ++ .../gradebook/score_confirm_delete.html | 13 + .../gradebook/score_create_form.html | 16 + .../templates/gradebook/score_form.html | 19 + .../templates/gradebook/search_results.html | 83 ++++ .../gradebook/subject_confirm_delete.html | 13 + .../gradebook/subject_create_form.html | 16 + .../templates/gradebook/subject_detail.html | 41 ++ .../templates/gradebook/subject_form.html | 19 + .../templates/gradebook/subject_list.html | 35 ++ .../gradebook/tag_confirm_delete.html | 13 + .../templates/gradebook/tag_create_form.html | 16 + .../templates/gradebook/tag_detail.html | 35 ++ .../templates/gradebook/tag_form.html | 16 + .../templates/gradebook/tag_list.html | 20 + src/gradebook/templatetags/__init__.py | 0 .../templatetags/gradebook_filters.py | 14 + src/gradebook/tests.py | 3 + src/gradebook/urls.py | 40 ++ src/gradebook/views.py | 297 +++++++++++ src/indici/asgi.py | 11 + src/indici/config.py | 20 + src/indici/middleware.py | 16 + src/indici/settings.py | 153 ++++++ src/indici/urls.py | 19 + src/indici/wsgi.py | 7 + src/manage.py | 22 + src/static/styles/main.css | 469 ++++++++++++++++++ src/static/styles/normalize.css | 350 +++++++++++++ src/students/__init__.py | 0 src/students/admin.py | 8 + src/students/apps.py | 6 + src/students/forms.py | 77 +++ src/students/migrations/0001_initial.py | 73 +++ .../migrations/0002_auto_20210912_0003.py | 23 + .../migrations/0003_alter_student_options.py | 17 + .../migrations/0004_auto_20210912_0030.py | 23 + src/students/migrations/__init__.py | 0 src/students/models.py | 131 +++++ .../students/message_confirm_delete.html | 13 + .../students/message_create_form.html | 16 + .../templates/students/message_detail.html | 8 + .../templates/students/message_form.html | 19 + .../templates/students/message_list.html | 19 + .../students/parent_confirm_delete.html | 13 + .../students/parent_create_form.html | 17 + .../templates/students/parent_detail.html | 63 +++ .../templates/students/parent_form.html | 19 + .../templates/students/parent_list.html | 19 + .../students/student_confirm_delete.html | 13 + .../students/student_create_form.html | 16 + .../templates/students/student_detail.html | 162 ++++++ .../templates/students/student_form.html | 19 + .../templates/students/student_list.html | 49 ++ .../students/thread_confirm_delete.html | 13 + .../students/thread_create_form.html | 16 + .../templates/students/thread_detail.html | 30 ++ .../templates/students/thread_form.html | 19 + .../templates/students/thread_list.html | 26 + src/students/templatetags/__init__.py | 0 src/students/templatetags/students_filters.py | 35 ++ src/students/tests.py | 3 + src/students/urls.py | 43 ++ src/students/views.py | 206 ++++++++ src/templates/base.html | 64 +++ src/templates/flatpages/base.html | 42 ++ src/templates/registration/logged_out.html | 9 + src/templates/registration/login.html | 33 ++ .../registration/password_change_done.html | 12 + .../registration/password_change_form.html | 14 + .../registration/password_reset_complete.html | 7 + .../registration/password_reset_confirm.html | 24 + .../registration/password_reset_done.html | 7 + .../registration/password_reset_email.html | 10 + .../registration/password_reset_form.html | 14 + templates/_confirm_delete.html | 13 + templates/_create_form.html | 12 + templates/_detail.html | 10 + templates/_form.html | 12 + templates/_list.html | 16 + 159 files changed, 6152 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 generate_templates.py create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 setup.yaml create mode 100644 src/accounts/__init__.py create mode 100644 src/accounts/admin.py create mode 100644 src/accounts/apps.py create mode 100644 src/accounts/forms.py create mode 100644 src/accounts/migrations/0001_initial.py create mode 100644 src/accounts/migrations/__init__.py create mode 100644 src/accounts/models.py create mode 100755 src/accounts/templates/accounts/account_create.html create mode 100755 src/accounts/templates/accounts/account_detail.html create mode 100755 src/accounts/templates/accounts/account_form.html create mode 100755 src/accounts/templates/accounts/account_list.html create mode 100644 src/accounts/templates/accounts/profile.html create mode 100755 src/accounts/templates/accounts/profile_form.html create mode 100644 src/accounts/tests.py create mode 100644 src/accounts/urls.py create mode 100644 src/accounts/views.py create mode 100644 src/attendance/__init__.py create mode 100644 src/attendance/admin.py create mode 100644 src/attendance/apps.py create mode 100644 src/attendance/forms.py create mode 100644 src/attendance/migrations/0001_initial.py create mode 100644 src/attendance/migrations/__init__.py create mode 100644 src/attendance/models.py create mode 100644 src/attendance/templates/attendance/day_confirm_delete.html create mode 100644 src/attendance/templates/attendance/day_create_form.html create mode 100644 src/attendance/templates/attendance/day_form.html create mode 100644 src/attendance/templates/attendance/day_list.html create mode 100644 src/attendance/templates/attendance/entry_confirm_delete.html create mode 100644 src/attendance/templates/attendance/entry_form.html create mode 100644 src/attendance/tests.py create mode 100644 src/attendance/urls.py create mode 100644 src/attendance/views.py create mode 100644 src/core/__init__.py create mode 100644 src/core/admin.py create mode 100644 src/core/apps.py create mode 100644 src/core/forms.py create mode 100644 src/core/migrations/0001_initial.py create mode 100644 src/core/migrations/0002_alter_schoolyear_start_date.py create mode 100644 src/core/migrations/0003_remove_schoolyear_end_date_and_more.py create mode 100644 src/core/migrations/0004_alter_schoolyear_year.py create mode 100644 src/core/migrations/0005_attendanceentry_created_at_and_more.py create mode 100644 src/core/migrations/0006_score.py create mode 100644 src/core/migrations/__init__.py create mode 100644 src/core/models.py create mode 100644 src/core/templates/core/partials/pagination.html create mode 100644 src/core/templates/core/schoolyear_confirm_delete.html create mode 100644 src/core/templates/core/schoolyear_create_form.html create mode 100644 src/core/templates/core/schoolyear_detail.html create mode 100644 src/core/templates/core/schoolyear_form.html create mode 100644 src/core/templates/core/schoolyear_list.html create mode 100644 src/core/templates/core/student_confirm_delete.html create mode 100644 src/core/templates/core/student_create_form.html create mode 100644 src/core/templates/core/student_detail.html create mode 100644 src/core/templates/core/student_form.html create mode 100644 src/core/templates/core/student_list.html create mode 100644 src/core/templatetags/__init__.py create mode 100644 src/core/templatetags/helpers.py create mode 100644 src/core/tests.py create mode 100644 src/core/urls.py create mode 100644 src/core/views.py create mode 100644 src/gradebook/__init__.py create mode 100644 src/gradebook/admin.py create mode 100644 src/gradebook/apps.py create mode 100644 src/gradebook/forms.py create mode 100644 src/gradebook/migrations/0001_initial.py create mode 100644 src/gradebook/migrations/0002_component_finished_grading.py create mode 100644 src/gradebook/migrations/__init__.py create mode 100644 src/gradebook/models.py create mode 100644 src/gradebook/templates/gradebook/component_confirm_delete.html create mode 100644 src/gradebook/templates/gradebook/component_create_form.html create mode 100644 src/gradebook/templates/gradebook/component_detail.html create mode 100644 src/gradebook/templates/gradebook/component_form.html create mode 100644 src/gradebook/templates/gradebook/component_manager.html create mode 100644 src/gradebook/templates/gradebook/score_confirm_delete.html create mode 100644 src/gradebook/templates/gradebook/score_create_form.html create mode 100644 src/gradebook/templates/gradebook/score_form.html create mode 100644 src/gradebook/templates/gradebook/search_results.html create mode 100644 src/gradebook/templates/gradebook/subject_confirm_delete.html create mode 100644 src/gradebook/templates/gradebook/subject_create_form.html create mode 100644 src/gradebook/templates/gradebook/subject_detail.html create mode 100644 src/gradebook/templates/gradebook/subject_form.html create mode 100644 src/gradebook/templates/gradebook/subject_list.html create mode 100644 src/gradebook/templates/gradebook/tag_confirm_delete.html create mode 100644 src/gradebook/templates/gradebook/tag_create_form.html create mode 100644 src/gradebook/templates/gradebook/tag_detail.html create mode 100644 src/gradebook/templates/gradebook/tag_form.html create mode 100644 src/gradebook/templates/gradebook/tag_list.html create mode 100644 src/gradebook/templatetags/__init__.py create mode 100644 src/gradebook/templatetags/gradebook_filters.py create mode 100644 src/gradebook/tests.py create mode 100644 src/gradebook/urls.py create mode 100644 src/gradebook/views.py create mode 100644 src/indici/asgi.py create mode 100644 src/indici/config.py create mode 100644 src/indici/middleware.py create mode 100644 src/indici/settings.py create mode 100644 src/indici/urls.py create mode 100644 src/indici/wsgi.py create mode 100755 src/manage.py create mode 100644 src/static/styles/main.css create mode 100644 src/static/styles/normalize.css create mode 100644 src/students/__init__.py create mode 100644 src/students/admin.py create mode 100644 src/students/apps.py create mode 100644 src/students/forms.py create mode 100644 src/students/migrations/0001_initial.py create mode 100644 src/students/migrations/0002_auto_20210912_0003.py create mode 100644 src/students/migrations/0003_alter_student_options.py create mode 100644 src/students/migrations/0004_auto_20210912_0030.py create mode 100644 src/students/migrations/__init__.py create mode 100644 src/students/models.py create mode 100644 src/students/templates/students/message_confirm_delete.html create mode 100644 src/students/templates/students/message_create_form.html create mode 100644 src/students/templates/students/message_detail.html create mode 100644 src/students/templates/students/message_form.html create mode 100644 src/students/templates/students/message_list.html create mode 100644 src/students/templates/students/parent_confirm_delete.html create mode 100644 src/students/templates/students/parent_create_form.html create mode 100644 src/students/templates/students/parent_detail.html create mode 100644 src/students/templates/students/parent_form.html create mode 100644 src/students/templates/students/parent_list.html create mode 100644 src/students/templates/students/student_confirm_delete.html create mode 100644 src/students/templates/students/student_create_form.html create mode 100644 src/students/templates/students/student_detail.html create mode 100644 src/students/templates/students/student_form.html create mode 100644 src/students/templates/students/student_list.html create mode 100644 src/students/templates/students/thread_confirm_delete.html create mode 100644 src/students/templates/students/thread_create_form.html create mode 100644 src/students/templates/students/thread_detail.html create mode 100644 src/students/templates/students/thread_form.html create mode 100644 src/students/templates/students/thread_list.html create mode 100644 src/students/templatetags/__init__.py create mode 100644 src/students/templatetags/students_filters.py create mode 100644 src/students/tests.py create mode 100644 src/students/urls.py create mode 100644 src/students/views.py create mode 100644 src/templates/base.html create mode 100644 src/templates/flatpages/base.html create mode 100755 src/templates/registration/logged_out.html create mode 100755 src/templates/registration/login.html create mode 100755 src/templates/registration/password_change_done.html create mode 100755 src/templates/registration/password_change_form.html create mode 100755 src/templates/registration/password_reset_complete.html create mode 100755 src/templates/registration/password_reset_confirm.html create mode 100755 src/templates/registration/password_reset_done.html create mode 100755 src/templates/registration/password_reset_email.html create mode 100755 src/templates/registration/password_reset_form.html create mode 100644 templates/_confirm_delete.html create mode 100644 templates/_create_form.html create mode 100644 templates/_detail.html create mode 100644 templates/_form.html create mode 100644 templates/_list.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be5686c --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.idea +./.idea +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +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/ +.pypirc +# 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 +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.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/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..55a3ef1 --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "*" +django-filter = "*" +python-dotenv = "*" +django-compressor = "*" +django-debug-toolbar = "*" + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..deaca44 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,135 @@ +{ + "_meta": { + "hash": { + "sha256": "87374a4c3741836c76d7a6aebe57dd8f1375eebf323b210aab0139c6d45b2266" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", + "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + ], + "markers": "python_version >= '3.7'", + "version": "==3.5.2" + }, + "django": { + "hashes": [ + "sha256:502ae42b6ab1b612c933fb50d5ff850facf858a4c212f76946ecd8ea5b3bf2d9", + "sha256:f7431a5de7277966f3785557c3928433347d998c1e6459324501378a291e5aab" + ], + "index": "pypi", + "version": "==4.0.5" + }, + "django-appconf": { + "hashes": [ + "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d", + "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.5" + }, + "django-compressor": { + "hashes": [ + "sha256:1db91b6d04293636a68bd1328dc7bb90d636b0295f67b1cc6d4fa102b9fd25f6", + "sha256:b4fe15cc23bf39420b37cb0030572bd0971104ca1ec3764f502c0f179e576dff" + ], + "index": "pypi", + "version": "==4.0" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661", + "sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5" + ], + "index": "pypi", + "version": "==3.5.0" + }, + "django-filter": { + "hashes": [ + "sha256:ed429e34760127e3520a67f415bec4c905d4649fbe45d0d6da37e6ff5e0287eb", + "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5" + ], + "index": "pypi", + "version": "==22.1" + }, + "python-dotenv": { + "hashes": [ + "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", + "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" + ], + "index": "pypi", + "version": "==0.20.0" + }, + "rcssmin": { + "hashes": [ + "sha256:0a6aae7e119509445bf7aa6da6ca0f285cc198273c20f470ad999ff83bbadcf9", + "sha256:1512223b6a687bb747e4e531187bd49a56ed71287e7ead9529cbaa1ca4718a0a", + "sha256:1d7c2719d014e4e4df4e33b75ae8067c7e246cf470eaec8585e06e2efac7586c", + "sha256:2211a5c91ea14a5937b57904c9121f8bfef20987825e55368143da7d25446e3b", + "sha256:27fc400627fd3d328b7fe95af2a01f5d0af6b5af39731af5d071826a1f08e362", + "sha256:30f5522285065cae0164d20068377d84b5d10b414156115f8729b034d0ea5e8b", + "sha256:32ccaebbbd4d56eab08cf26aed36f5d33389b9d1d3ca1fecf53eb6ab77760ddf", + "sha256:352dd3a78eb914bb1cb269ac2b66b3154f2490a52ab605558c681de3fb5194d2", + "sha256:37f1242e34ca273ed2c26cf778854e18dd11b31c6bfca60e23fce146c84667c1", + "sha256:49807735f26f59404194f1e6f93254b6d5b6f7748c2a954f4470a86a40ff4c13", + "sha256:506e33ab4c47051f7deae35b6d8dbb4a5c025f016e90a830929a1ecc7daa1682", + "sha256:6158d0d86cd611c5304d738dc3d6cfeb23864dd78ad0d83a633f443696ac5d77", + "sha256:7085d1b51dd2556f3aae03947380f6e9e1da29fb1eeadfa6766b7f105c54c9ff", + "sha256:7c44002b79f3656348196005b9522ec5e04f182b466f66d72b16be0bd03c13d8", + "sha256:7da63fee37edf204bbd86785edb4d7491642adbfd1d36fd230b7ccbbd8db1a6f", + "sha256:8b659a88850e772c84cfac4520ec223de6807875e173d8ef3248ab7f90876066", + "sha256:c28b9eb20982b45ebe6adef8bd2547e5ed314dafddfff4eba806b0f8c166cfd1", + "sha256:ddff3a41611664c7f1d9e3d8a9c1669e0e155ac0458e586ffa834dc5953e7d9f", + "sha256:f1a37bbd36b050813673e62ae6464467548628690bf4d48a938170e121e8616e", + "sha256:f31c82d06ba2dbf33c20db9550157e80bb0c4cbd24575c098f0831d1d2e3c5df" + ], + "version": "==1.1.0" + }, + "rjsmin": { + "hashes": [ + "sha256:05efa485dfddb6418e3b86d8862463aa15641a61f6ae05e7e6de8f116ee77c69", + "sha256:1622fbb6c6a8daaf77da13cc83356539bfe79c1440f9664b02c7f7b150b9a18e", + "sha256:1c93b29fd725e61718299ffe57de93ff32d71b313eaabbfcc7bd32ddb82831d5", + "sha256:2ed83aca637186bafdc894b4b7fc3657e2d74014ccca7d3d69122c1e82675216", + "sha256:38a4474ed52e1575fb9da983ec8657faecd8ab3738508d36e04f87769411fd3d", + "sha256:3b14f4c2933ec194eb816b71a0854ce461b6419a3d852bf360344731ab28c0a6", + "sha256:40e7211a25d9a11ac9ff50446e41268c978555676828af86fa1866615823bfff", + "sha256:41c7c3910f7b8816e37366b293e576ddecf696c5f2197d53cf2c1526ac336646", + "sha256:4387a00777faddf853eebdece9f2e56ebaf243c3f24676a9de6a20c5d4f3d731", + "sha256:54fc30519365841b27556ccc1cb94c5b4413c384ff6d467442fddba66e2e325a", + "sha256:6c395ffc130332cca744f081ed5efd5699038dcb7a5d30c3ff4bc6adb5b30a62", + "sha256:6c529feb6c400984452494c52dd9fdf59185afeacca2afc5174a28ab37751a1b", + "sha256:86c4da7285ddafe6888cb262da563570f28e4a31146b5164a7a6947b1222196b", + "sha256:8944a8a55ac825b8e5ec29f341ecb7574697691ef416506885898d2f780fb4ca", + "sha256:993935654c1311280e69665367d7e6ff694ac9e1609168cf51cae8c0307df0db", + "sha256:99e5597a812b60058baa1457387dc79cca7d273b2a700dc98bfd20d43d60711d", + "sha256:b6a7c8c8d19e154334f640954e43e57283e87bb4a2f6e23295db14eea8e9fc1d", + "sha256:c81229ffe5b0a0d5b3b5d5e6d0431f182572de9e9a077e85dbae5757db0ab75c", + "sha256:d63e193a2f932a786ae82068aa76d1d126fcdff8582094caff9e5e66c4dcc124", + "sha256:e18fe1a610fb105273bb369f61c2b0bd9e66a3f0792e27e4cac44e42ace1968b" + ], + "version": "==1.2.0" + }, + "sqlparse": { + "hashes": [ + "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", + "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.2" + } + }, + "develop": {} +} diff --git a/generate_templates.py b/generate_templates.py new file mode 100644 index 0000000..42f0258 --- /dev/null +++ b/generate_templates.py @@ -0,0 +1,63 @@ +import sys +import os +import shutil +import logging + +logging.basicConfig( + format='[%(asctime)s] %(levelname)s: %(message)s', + level=logging.INFO, + datefmt='%m/%d/%Y %H:%M:%S' +) + +logger = logging.getLogger(__name__) + + +TEMPLATES = [ + '_confirm_delete.html', + '_create_form.html', + '_detail.html', + '_form.html', + '_list.html', +] + + +def main(): + app = sys.argv[1] + name = sys.argv[2] + destination = f'src/{app}/templates/{app}/' + sourcedir = 'templates' + copy_files_to_folder(sourcedir, TEMPLATES, destination, name) + for file in TEMPLATES: + replace_text_in_file( + f'{destination}{name}{file}', + 'object_url', + f'{app}:{name}' + ) + replace_text_in_file( + f'{destination}{name}{file}', + 'object_name', + name.capitalize() + ) + replace_text_in_file(f'{destination}{name}{file}', 'object', name) + + +def copy_files_to_folder(sourcedir, files, destination, prefix): + for f in files: + logger.info(f'Copying "{sourcedir}/{f}" to "{destination}{prefix}{f}"') + shutil.copy(f'{sourcedir}/{f}', f'{destination}{prefix}{f}') + + +def replace_text_in_file(file, search_txt, replace_txt): + logger.info( + f'Searching file for "{search_txt}" and replacing with "{replace_txt}"' + ) + with open(file, 'r') as f: + data = f.read() + data = data.replace(search_txt, replace_txt) + + with open(file, 'w') as f: + f.write(data) + + +if __name__ == '__main__': + main() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6714149 --- /dev/null +++ b/readme.md @@ -0,0 +1,25 @@ +# Indici + +## How To Start + + +### 1. Activate Virtualenv + +`windows` +```cmd + /venv/scripts/activate +``` +>Your Current Working Directory + +`Ubuntu [Debian]` +```commandline +source venv/bin/activate +``` +>you can use any name instead of **venv** + +### 2. Runserver +``` +python3 src/manage.py runserver +``` + +> Built Using [django-cli](https://github.com/khan-asfi-reza/django-setup-cli) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6e3a53f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,69 @@ +applicationinsights==0.11.10 +asgiref==3.5.2 +atomicwrites==1.4.0 +backports.csv==1.0.7 +bleach==5.0.0 +build==0.8.0 +certifi==2021.10.8 +cffi==1.15.0 +charset-normalizer==2.0.12 +cli-helpers==0.2.3 +Click==7.0 +click-log==0.4.0 +commonmark==0.9.1 +configobj==5.0.6 +cryptography==37.0.2 +distlib==0.3.4 +Django==4.0.4 +django-setup-cli==1.0.17 +docutils==0.18.1 +enum34==1.1.10 +filelock==3.7.0 +future==0.18.2 +humanize==4.1.0 +icalendar==4.0.9 +idna==3.3 +importlib-metadata==4.11.4 +jeepney==0.8.0 +Jinja2==3.1.2 +keyring==23.5.1 +khal==0.10.4 +MarkupSafe==2.1.1 +mssql-cli==1.0.0 +packaging==21.3 +pep517==0.12.0 +pipenv==2022.5.2 +pkginfo==1.8.2 +platformdirs==2.5.2 +prettytable==3.3.0 +prompt-toolkit==2.0.10 +pycparser==2.21 +Pygments==2.12.0 +pymssql==2.2.5 +pyparsing==3.0.9 +python-dateutil==2.8.2 +python-dotenv==0.19.2 +pytz==2022.1 +pytz-deprecation-shim==0.1.0.post0 +pyxdg==0.27 +PyYAML==6.0 +readme-renderer==35.0 +requests==2.27.1 +requests-toolbelt==0.9.1 +rfc3986==2.0.0 +rich==12.4.4 +SecretStorage==3.3.2 +six==1.16.0 +sqlparse==0.2.4 +terminaltables==3.1.10 +tomli==2.0.1 +twine==4.0.1 +tzdata==2022.1 +tzlocal==4.2 +urllib3==1.26.9 +urwid==2.1.2 +virtualenv==20.14.1 +virtualenv-clone==0.5.7 +wcwidth==0.2.5 +webencodings==0.5.1 +zipp==3.8.0 diff --git a/setup.yaml b/setup.yaml new file mode 100644 index 0000000..a7a6fdc --- /dev/null +++ b/setup.yaml @@ -0,0 +1,17 @@ +name: indici +author: Nathan Chapman +description: Gradebook and attendance tracking +static: true +template: true +database: + port: $DATABASE_PORT + host: $DATABASE_HOST + password: $DATABASE_PASSWORD + user: $DATABASE_USER + name: $DATABASE_NAME + engine: $DATABASE_ENGINE +cache: + location: $CACHE_LOCATION + backend: $CACHE_BACKEND +env: + SECRET_KEY: $SECRET_KEY diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/admin.py b/src/accounts/admin.py new file mode 100644 index 0000000..f91be8f --- /dev/null +++ b/src/accounts/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + +admin.site.register(User, UserAdmin) diff --git a/src/accounts/apps.py b/src/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/src/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/src/accounts/forms.py b/src/accounts/forms.py new file mode 100644 index 0000000..ea33b3c --- /dev/null +++ b/src/accounts/forms.py @@ -0,0 +1,13 @@ +from django import forms +from .models import User + + +class AccountUpdateForm(forms.ModelForm): + class Meta: + model = User + fields = [ + 'first_name', + 'last_name', + 'email', + 'timezone', + ] diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..0f7c2da --- /dev/null +++ b/src/accounts/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.0.5 on 2022-06-29 16:38 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('timezone', models.CharField(choices=[('US/Alaska', 'US - Alaska'), ('US/Arizona', 'US - Arizona'), ('US/Central', 'US - Central'), ('US/Eastern', 'US - Eastern'), ('US/Hawaii', 'US - Hawaii'), ('US/Mountain', 'US - Mountain'), ('US/Pacific', 'US - Pacific')], default='US/Mountain', max_length=50)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/src/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/models.py b/src/accounts/models.py new file mode 100644 index 0000000..8e807e8 --- /dev/null +++ b/src/accounts/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + + +class User(AbstractUser): + ALASKA = 'US/Alaska' + ARIZONA = 'US/Arizona' + CENTRAL = 'US/Central' + EASTERN = 'US/Eastern' + HAWAII = 'US/Hawaii' + MOUNTAIN = 'US/Mountain' + PACIFIC = 'US/Pacific' + + TIMEZONE_CHOICES = [ + (ALASKA, 'US - Alaska'), + (ARIZONA, 'US - Arizona'), + (CENTRAL, 'US - Central'), + (EASTERN, 'US - Eastern'), + (HAWAII, 'US - Hawaii'), + (MOUNTAIN, 'US - Mountain'), + (PACIFIC, 'US - Pacific'), + ] + timezone = models.CharField( + max_length=50, + choices=TIMEZONE_CHOICES, + default=MOUNTAIN + ) diff --git a/src/accounts/templates/accounts/account_create.html b/src/accounts/templates/accounts/account_create.html new file mode 100755 index 0000000..8bee1d3 --- /dev/null +++ b/src/accounts/templates/accounts/account_create.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Sign up

+ +
+ {% csrf_token %} + {{ form.as_p }} + + +
+
+{% endblock %} diff --git a/src/accounts/templates/accounts/account_detail.html b/src/accounts/templates/accounts/account_detail.html new file mode 100755 index 0000000..fdbd203 --- /dev/null +++ b/src/accounts/templates/accounts/account_detail.html @@ -0,0 +1,72 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Welcome {{user.first_name}} {{user.last_name }} +
+ Here's what's going on today +

+
+
+

Birthdays

+
    + {% for student in birthdays %} +
  • {{student}} is turning {{student.age|add:1}} on {{student.dob|date:"M j"}}
  • + {% empty %} +

    No Birthdays this next week.

    + {% endfor %} +
+
+
+

Today's Assignments

+
    + {% for component in components %} +
  • + {{component.subject}}, {{component}} +
  • + {% empty %} +

    Nothing for today.

    + {% endfor %} +
+
+
+

Today's Attendance

+ {% for day in attendance %} +

{{day.date}}

+ + + + + + + + + {% for entry in day.entry_set.all %} + + + + + + {% endfor %} + +
StudentStatus
{{entry.student}}{{entry.get_status_display}}Update
+ {% empty %} +

No attendance taken yet: Take attendance

+ {% endfor %} +
+
+

Assignments to be graded

+
    + {% for component in ungraded_components %} +
  • + {{component.subject}}, {{component}} +
  • + {% empty %} +

    Everything is graded to far.

    + {% endfor %} +
+
+
+{% endblock %} diff --git a/src/accounts/templates/accounts/account_form.html b/src/accounts/templates/accounts/account_form.html new file mode 100755 index 0000000..f40c5eb --- /dev/null +++ b/src/accounts/templates/accounts/account_form.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} + +{% endblock %} diff --git a/src/accounts/templates/accounts/account_list.html b/src/accounts/templates/accounts/account_list.html new file mode 100755 index 0000000..e3d9893 --- /dev/null +++ b/src/accounts/templates/accounts/account_list.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Users

+ + + + + + + {% for user in user_list %} + + + + + {% empty %} + + {% endfor %} + +
UsernameName
{{ user.username }}{{user.first_name}} {{user.last_name}}
No users yet.
+
+{% endblock %} diff --git a/src/accounts/templates/accounts/profile.html b/src/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..6a4349d --- /dev/null +++ b/src/accounts/templates/accounts/profile.html @@ -0,0 +1,72 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Welcome {{profile.user.first_name}} {{profile.user.last_name }} +
+ Here's what's going on today +

+
+
+

Birthdays

+
    + {% for student in birthdays %} +
  • {{student}} is turning {{student.age|add:1}} on {{student.dob|date:"M j"}}
  • + {% empty %} +

    No Birthdays this next week.

    + {% endfor %} +
+
+
+

Today's Assignments

+
    + {% for component in components %} +
  • + {{component.subject}}, {{component}} +
  • + {% empty %} +

    Nothing for today.

    + {% endfor %} +
+
+
+

Today's Attendance

+ {% for day in attendance %} +

{{day.date}}

+ + + + + + + + + {% for entry in day.entry_set.all %} + + + + + + {% endfor %} + +
StudentStatus
{{entry.student}}{{entry.get_status_display}}Update
+ {% empty %} +

No attendance taken yet: Take attendance

+ {% endfor %} +
+
+

Assignments to be graded

+
    + {% for component in ungraded_components %} +
  • + {{component.subject}}, {{component}} +
  • + {% empty %} +

    Everything is graded to far.

    + {% endfor %} +
+
+
+{% endblock %} diff --git a/src/accounts/templates/accounts/profile_form.html b/src/accounts/templates/accounts/profile_form.html new file mode 100755 index 0000000..9b1dd9e --- /dev/null +++ b/src/accounts/templates/accounts/profile_form.html @@ -0,0 +1,10 @@ +
+

Settings

+
+ {% csrf_token %} + {{form.as_p}} +

+ +

+
+
diff --git a/src/accounts/tests.py b/src/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/accounts/urls.py b/src/accounts/urls.py new file mode 100644 index 0000000..4f89f4f --- /dev/null +++ b/src/accounts/urls.py @@ -0,0 +1,11 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + path('', views.AccountListView.as_view(), name='account-list'), + path('/', include([ + path('', views.AccountDetailView.as_view(), name='account-detail'), + path('update/', views.AccountUpdateView.as_view(), name='account-update'), + path('delete/', views.AccountDeleteView.as_view(), name='account-delete'), + ])), +] diff --git a/src/accounts/views.py b/src/accounts/views.py new file mode 100644 index 0000000..d5e8bc7 --- /dev/null +++ b/src/accounts/views.py @@ -0,0 +1,75 @@ +import zoneinfo +import datetime as dt +from django.utils import timezone +from django.shortcuts import render, reverse, redirect +from django.db.models import Avg, Count, Min, Sum +from django.urls import reverse_lazy +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.forms import PasswordChangeForm + +from students.models import Student +from gradebook.models import Component +from attendance.models import Day, Entry + +from .models import User +from .forms import AccountUpdateForm + + +class AccountListView(LoginRequiredMixin, ListView): + model = User + template_name = 'accounts/account_list.html' + + +class AccountDetailView(LoginRequiredMixin, DetailView): + model = User + template_name = 'accounts/account_detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + today = timezone.localtime(timezone.now()).date() + enddate = today + dt.timedelta(days=7) + + context['birthdays'] = Student.objects.filter( + dob__month=today.month, + dob__day__range=[today.day, enddate.day] + ).order_by('dob') + + context['components'] = Component.objects.filter( + due_date=today + ).select_related('subject') + + context['attendance'] = Day.objects.filter( + date=today + ).prefetch_related('entry_set', 'entry_set__student') + + context['ungraded_components'] = Component.objects.filter( + due_date__lte=today, + finished_grading=False + ).select_related('subject') + + return context + + +class AccountUpdateView(LoginRequiredMixin, UpdateView): + model = User + form_class = AccountUpdateForm + template_name = 'accounts/account_form.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['timezones'] = User.TIMEZONE_CHOICES + return context + + def get_success_url(self): + pk = self.kwargs["pk"] + return reverse('account-detail', kwargs={'pk': pk}) + + +class AccountDeleteView(LoginRequiredMixin, DeleteView): + model = User + success_url = reverse_lazy('account-list') diff --git a/src/attendance/__init__.py b/src/attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/attendance/admin.py b/src/attendance/admin.py new file mode 100644 index 0000000..186b2af --- /dev/null +++ b/src/attendance/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import Day, Entry + +admin.site.register(Day) +admin.site.register(Entry) diff --git a/src/attendance/apps.py b/src/attendance/apps.py new file mode 100644 index 0000000..ba31b45 --- /dev/null +++ b/src/attendance/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AttendanceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'attendance' diff --git a/src/attendance/forms.py b/src/attendance/forms.py new file mode 100644 index 0000000..5a040a5 --- /dev/null +++ b/src/attendance/forms.py @@ -0,0 +1,24 @@ +from django import forms +from django.utils import timezone +from .models import Day, Entry + +from students.models import Student + + +class DayForm(forms.ModelForm): + class Meta: + model = Day + fields = ('date',) + widgets = { + 'date': forms.DateInput(attrs = { + 'type': 'date', + }), + } + +class EntryForm(forms.ModelForm): + class Meta: + model = Entry + fields = ('day', 'student', 'status') + widgets = { + 'student': forms.HiddenInput() + } diff --git a/src/attendance/migrations/0001_initial.py b/src/attendance/migrations/0001_initial.py new file mode 100644 index 0000000..8ed891f --- /dev/null +++ b/src/attendance/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.7 on 2021-09-01 15:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('students', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Day', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ], + options={ + 'ordering': ('-date',), + }, + ), + migrations.CreateModel( + name='Entry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('P', 'Present'), ('T', 'Tardy'), ('A', 'Absent')], default='P', max_length=1)), + ('day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='attendance.day')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='students.student')), + ], + options={ + 'verbose_name_plural': 'entries', + 'ordering': ('student',), + }, + ), + ] diff --git a/src/attendance/migrations/__init__.py b/src/attendance/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/attendance/models.py b/src/attendance/models.py new file mode 100644 index 0000000..aba8f43 --- /dev/null +++ b/src/attendance/models.py @@ -0,0 +1,33 @@ +from django.db import models +from students.models import Student + +class Day(models.Model): + class Meta: + ordering = ('-date',) + + date = models.DateField() + + def __str__(self): + return f"{self.date}" + +class Entry(models.Model): + class Meta: + verbose_name_plural = 'entries' + ordering = ('student',) + + STATUS_CHOICES = [ + ('P', 'Present'), + ('T', 'Tardy'), + ('A', 'Absent'), + ] + + day = models.ForeignKey(Day, on_delete=models.CASCADE) + student = models.ForeignKey(Student, on_delete=models.CASCADE) + status = models.CharField( + max_length=1, + choices=STATUS_CHOICES, + default='P' + ) + + def __str__(self): + return f"{self.day} | {self.student} | {self.status}" diff --git a/src/attendance/templates/attendance/day_confirm_delete.html b/src/attendance/templates/attendance/day_confirm_delete.html new file mode 100644 index 0000000..29a8cf1 --- /dev/null +++ b/src/attendance/templates/attendance/day_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{day}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/attendance/templates/attendance/day_create_form.html b/src/attendance/templates/attendance/day_create_form.html new file mode 100644 index 0000000..fe38267 --- /dev/null +++ b/src/attendance/templates/attendance/day_create_form.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ {% csrf_token %} +
+

Take Attendance

+ {{form.as_p}} +
+ + + + + + + + + + + {% for student in student_list %} + + + + + + + {% endfor %} + +
StudentPresentTardyAbsent
{{student.full_name}}
+ or cancel +
+
+{% endblock %} diff --git a/src/attendance/templates/attendance/day_form.html b/src/attendance/templates/attendance/day_form.html new file mode 100644 index 0000000..9ea345e --- /dev/null +++ b/src/attendance/templates/attendance/day_form.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ {% csrf_token %} +
+

Take Attendance

+ {{form.as_p}} +
+ + + + + + + + + + + {% for entry in day.entry_set.all %} + + + + + + + {% endfor %} + +
StudentPresentTardyAbsent
{{entry.student.full_name}}
+ or cancel +
+
+{% endblock %} diff --git a/src/attendance/templates/attendance/day_list.html b/src/attendance/templates/attendance/day_list.html new file mode 100644 index 0000000..2ef3a71 --- /dev/null +++ b/src/attendance/templates/attendance/day_list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block content %} +
+

Attendance

+

+ Take attendance +

+
+ {% for day in day_list %} +

{{day.date}}

+ + + + + + + + + {% for entry in day.entry_set.all %} + + + + + + {% endfor %} + +
StudentStatus
{{entry.student}}{{entry.get_status_display}}Update
+ {% empty %} +

No attendance taken yet.

+ {% endfor %} +
+
+ + {% if page_obj.has_previous %} + « first + ‹ previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next › + last » + {% endif %} + +
+
+{% endblock %} \ No newline at end of file diff --git a/src/attendance/templates/attendance/entry_confirm_delete.html b/src/attendance/templates/attendance/entry_confirm_delete.html new file mode 100644 index 0000000..a640505 --- /dev/null +++ b/src/attendance/templates/attendance/entry_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{entry}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/attendance/templates/attendance/entry_form.html b/src/attendance/templates/attendance/entry_form.html new file mode 100644 index 0000000..ba63152 --- /dev/null +++ b/src/attendance/templates/attendance/entry_form.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Update Entry

+ Delete {{entry}} +
+

For {{student}}

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/attendance/tests.py b/src/attendance/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/attendance/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/attendance/urls.py b/src/attendance/urls.py new file mode 100644 index 0000000..b77b6ff --- /dev/null +++ b/src/attendance/urls.py @@ -0,0 +1,20 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + path('days/', views.DayListView.as_view(), name='day-list'), + path('days/new/', views.DayCreateView.as_view(), name='day-create'), + path('days//', include([ + path('', views.DayDetailView.as_view(), name='day-detail'), + path('update/', views.DayUpdateView.as_view(), name='day-update'), + path('delete/', views.DayDeleteView.as_view(), name='day-delete'), + ])), + + path('entries/', views.EntryListView.as_view(), name='entry-list'), + path('entries/new/', views.EntryCreateView.as_view(), name='entry-create'), + path('entries//', include([ + path('', views.EntryDetailView.as_view(), name='entry-detail'), + path('update/', views.EntryUpdateView.as_view(), name='entry-update'), + path('delete/', views.EntryDeleteView.as_view(), name='entry-delete'), + ])), +] diff --git a/src/attendance/views.py b/src/attendance/views.py new file mode 100644 index 0000000..d45ff60 --- /dev/null +++ b/src/attendance/views.py @@ -0,0 +1,98 @@ +from django.shortcuts import render +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy, reverse +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils import timezone + +from django.db.models import Prefetch, Subquery, Count, Sum, F, Q, Value +from django.db.models.functions import Length, Upper + +from students.models import Student +from .models import Day, Entry +from .forms import DayForm, EntryForm + + +# Days +class DayListView(LoginRequiredMixin, ListView): + model = Day + paginate_by = 7 + + def get_queryset(self): + object_list = Day.objects.all().prefetch_related('entry_set', 'entry_set__student') + return object_list + +class DayCreateView(LoginRequiredMixin, CreateView): + model = Day + template_name_suffix = '_create_form' + form_class = DayForm + success_url = reverse_lazy('profile-detail') + + def get_initial(self): + today = timezone.localtime(timezone.now()).date() + initial = { + 'date': today, + } + return initial + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['student_list'] = Student.objects.all() + return context + + def form_valid(self, form): + form.save() + for key, value in self.request.POST.items(): + if 'student' in key: + s = key.split('_')[1] + Entry.objects.create( + day=form.instance, + student=Student.objects.get(pk=s), + status=value, + ) + return super().form_valid(form) + +class DayDetailView(LoginRequiredMixin, DetailView): + model = Day + +class DayUpdateView(LoginRequiredMixin, UpdateView): + model = Day + form_class = DayForm + success_url = reverse_lazy('day-list') + + def form_valid(self, form): + form.save() + for key, value in self.request.POST.items(): + if 'student' in key: + s = key.split('_')[1] + Entry.objects.filter(day=self.object, student=s).update(status=value) + return super().form_valid(form) + +class DayDeleteView(LoginRequiredMixin, DeleteView): + model = Day + success_url = reverse_lazy('day-list') + + +# Entries +class EntryListView(LoginRequiredMixin, ListView): + model = Entry + +class EntryCreateView(LoginRequiredMixin, CreateView): + model = Entry + template_name_suffix = '_create_form' + fields = ('__all__') + +class EntryDetailView(LoginRequiredMixin, DetailView): + model = Entry + +class EntryUpdateView(LoginRequiredMixin, UpdateView): + model = Entry + form_class = EntryForm + success_url = reverse_lazy('day-list') + +class EntryDeleteView(LoginRequiredMixin, DeleteView): + model = Entry + success_url = reverse_lazy('day-list') diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/admin.py b/src/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/src/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/core/apps.py b/src/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/src/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/src/core/forms.py b/src/core/forms.py new file mode 100644 index 0000000..7a137b5 --- /dev/null +++ b/src/core/forms.py @@ -0,0 +1,37 @@ +from django import forms +from django.utils import timezone + +from .models import ( + SchoolYear, + StudentTag, + Student, + Subject, + Tag, + Component, + SchoolDay, + AttendanceEntry, +) + + +class SchoolYearCreateForm(forms.ModelForm): + class Meta: + model = SchoolYear + fields = [ + 'year' + ] + + +class StudentForm(forms.ModelForm): + class Meta: + model = Student + fields = ( + 'student_id', + 'first_name', + 'last_name', + 'address', + 'dob', + ) + labels = { + 'student_id': 'Student ID', + 'dob': 'DOB', + } diff --git a/src/core/migrations/0001_initial.py b/src/core/migrations/0001_initial.py new file mode 100644 index 0000000..fd3ee3f --- /dev/null +++ b/src/core/migrations/0001_initial.py @@ -0,0 +1,114 @@ +# Generated by Django 4.0.5 on 2022-06-29 18:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='SchoolYear', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Subject', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=250)), + ('description', models.CharField(blank=True, max_length=250)), + ('school_year', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.schoolyear')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='StudentTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.SlugField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + ), + migrations.CreateModel( + name='Student', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('student_id', models.IntegerField()), + ('first_name', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('address', models.TextField(blank=True)), + ('dob', models.DateField()), + ('school_year', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.schoolyear')), + ], + options={ + 'ordering': ['student_id', 'first_name'], + }, + ), + migrations.CreateModel( + name='SchoolDay', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('school_year', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.schoolyear')), + ], + options={ + 'ordering': ['-date'], + }, + ), + migrations.CreateModel( + name='Component', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('category', models.CharField(choices=[('QUIZ', 'Quiz'), ('ASSIGNMENT', 'Assignment'), ('TEST', 'Test')], default='ASSIGNMENT', max_length=255)), + ('due_date', models.DateField()), + ('grade_total', models.PositiveIntegerField()), + ('finished_grading', models.BooleanField(default=False)), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.subject')), + ('tags', models.ManyToManyField(blank=True, to='core.tag')), + ], + options={ + 'ordering': ['due_date'], + }, + ), + migrations.CreateModel( + name='AttendanceEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('P', 'Present'), ('T', 'Tardy'), ('A', 'Absent')], default='P', max_length=1)), + ('school_day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.schoolday')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.student')), + ], + options={ + 'verbose_name_plural': 'entries', + 'ordering': ['student'], + }, + ), + migrations.AddIndex( + model_name='studenttag', + index=models.Index(fields=['content_type', 'object_id'], name='core_studen_content_a7305d_idx'), + ), + ] diff --git a/src/core/migrations/0002_alter_schoolyear_start_date.py b/src/core/migrations/0002_alter_schoolyear_start_date.py new file mode 100644 index 0000000..07bad54 --- /dev/null +++ b/src/core/migrations/0002_alter_schoolyear_start_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2022-06-29 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='schoolyear', + name='start_date', + field=models.DateField(unique_for_year=True), + ), + ] diff --git a/src/core/migrations/0003_remove_schoolyear_end_date_and_more.py b/src/core/migrations/0003_remove_schoolyear_end_date_and_more.py new file mode 100644 index 0000000..d7b4c26 --- /dev/null +++ b/src/core/migrations/0003_remove_schoolyear_end_date_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.5 on 2022-06-29 20:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_schoolyear_start_date'), + ] + + operations = [ + migrations.RemoveField( + model_name='schoolyear', + name='end_date', + ), + migrations.RemoveField( + model_name='schoolyear', + name='start_date', + ), + migrations.AddField( + model_name='schoolyear', + name='year', + field=models.PositiveIntegerField(default=2021), + preserve_default=False, + ), + ] diff --git a/src/core/migrations/0004_alter_schoolyear_year.py b/src/core/migrations/0004_alter_schoolyear_year.py new file mode 100644 index 0000000..fc6665d --- /dev/null +++ b/src/core/migrations/0004_alter_schoolyear_year.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2022-06-29 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_remove_schoolyear_end_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='schoolyear', + name='year', + field=models.PositiveIntegerField(unique=True), + ), + ] diff --git a/src/core/migrations/0005_attendanceentry_created_at_and_more.py b/src/core/migrations/0005_attendanceentry_created_at_and_more.py new file mode 100644 index 0000000..60cf571 --- /dev/null +++ b/src/core/migrations/0005_attendanceentry_created_at_and_more.py @@ -0,0 +1,104 @@ +# Generated by Django 4.0.5 on 2022-06-29 21:27 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_alter_schoolyear_year'), + ] + + operations = [ + migrations.AddField( + model_name='attendanceentry', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='attendanceentry', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='component', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='component', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='schoolday', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='schoolday', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='schoolyear', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='schoolyear', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='student', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2022, 6, 29, 21, 26, 47, 547231, tzinfo=utc)), + preserve_default=False, + ), + migrations.AddField( + model_name='student', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='studenttag', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='studenttag', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='subject', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='subject', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='tag', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='tag', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/src/core/migrations/0006_score.py b/src/core/migrations/0006_score.py new file mode 100644 index 0000000..35338e0 --- /dev/null +++ b/src/core/migrations/0006_score.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.5 on 2022-06-30 20:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_attendanceentry_created_at_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Score', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.PositiveIntegerField()), + ('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.component')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.student')), + ], + options={ + 'ordering': ['student'], + }, + ), + ] diff --git a/src/core/migrations/__init__.py b/src/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/models.py b/src/core/models.py new file mode 100644 index 0000000..5a54a2c --- /dev/null +++ b/src/core/models.py @@ -0,0 +1,259 @@ +from datetime import date +from django.db import models +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import ( + GenericForeignKey, GenericRelation +) + + +class SchoolYear(models.Model): + year = models.PositiveIntegerField(unique=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f'{self.year}' + + @property + def is_current_year(self): + return True if self.year == int(date.today().year) else False + + def get_absolute_url(self): + return reverse('core:schoolyear-detail', kwargs={ + 'year': self.year + }) + + +class StudentTag(models.Model): + tag = models.SlugField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.tag + + class Meta: + indexes = [ + models.Index(fields=['content_type', 'object_id']), + ] + + +# class StudentManager(models.Manager): +# def get_queryset(self): +# today = date.today() +# return super().get_queryset().filter( +# school_year__start_date__year__gte=today.year, +# school_year__end_date__year__lte=today.year +# ) + + +class Student(models.Model): + school_year = models.ForeignKey( + SchoolYear, + blank=True, + null=True, + on_delete=models.SET_NULL + ) + + student_id = models.IntegerField() + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + address = models.TextField(blank=True) + dob = models.DateField() + + tags = GenericRelation(StudentTag) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @property + def full_name(self): + return f'{self.first_name} {self.last_name}' + + @property + def age(self): + today = date.today() + return today.year - self.dob.year - ( + (today.month, today.day) < (self.dob.month, self.dob.day) + ) + + def __str__(self): + return f'{self.first_name} {self.last_name}' + + def get_absolute_url(self): + return reverse('core:student-detail', kwargs={ + 'year': self.school_year.year, + 'student_pk': self.pk + }) + + class Meta: + ordering = ['student_id', 'first_name'] + + +class Subject(models.Model): + school_year = models.ForeignKey( + SchoolYear, + blank=True, + null=True, + on_delete=models.SET_NULL + ) + + name = models.CharField(max_length=250) + description = models.CharField(max_length=250, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('subject-detail', kwargs={'pk': self.pk}) + + class Meta: + ordering = ['name'] + + +class Tag(models.Model): + name = models.CharField(max_length=50) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('tag-detail', kwargs={'pk': self.pk}) + + class Meta: + ordering = ['name'] + + +class Component(models.Model): + QZ = 'QUIZ' + AS = 'ASSIGNMENT' + TS = 'TEST' + + CATEGORY_CHOICES = [ + (QZ, 'Quiz'), + (AS, 'Assignment'), + (TS, 'Test'), + ] + + subject = models.ForeignKey(Subject, on_delete=models.CASCADE) + name = models.CharField(max_length=50) + category = models.CharField( + max_length=255, + choices=CATEGORY_CHOICES, + default=AS, + ) + due_date = models.DateField() + grade_total = models.PositiveIntegerField() + finished_grading = models.BooleanField(default=False) + tags = models.ManyToManyField(Tag, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @property + def is_due(self): + return True if self.due_date < date.today() else False + + @property + def grade_avg(self): + avg = Score.objects.filter(component=self.pk).aggregate( + Avg('value') + ) + if avg['value__avg'] is not None: + return round(avg['value__avg'], 2) + else: + return 'No scores yet.' + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('component-detail', kwargs={'pk': self.pk}) + + class Meta: + ordering = ['due_date'] + + +class Score(models.Model): + component = models.ForeignKey(Component, on_delete=models.CASCADE) + student = models.ForeignKey(Student, on_delete=models.CASCADE) + value = models.PositiveIntegerField() + + @property + def grade(self): + return f'{self.value} / {self.component.grade_total}' + + @property + def grade_as_percentage(self): + return round(self.value / self.component.grade_total * 100, 2) + + def __str__(self): + return f'{self.student} scored: {self.value} / {self.component.grade_total}' + + def get_absolute_url(self): + return reverse('score-detail', kwargs={'pk': self.pk}) + + class Meta: + ordering = ['student'] + + +class SchoolDay(models.Model): + school_year = models.ForeignKey( + SchoolYear, + blank=True, + null=True, + on_delete=models.SET_NULL + ) + + date = models.DateField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f'{self.date}' + + class Meta: + ordering = ['-date'] + + +class AttendanceEntry(models.Model): + PRESENT = 'P' + TARDY = 'T' + ABSENT = 'A' + + STATUS_CHOICES = [ + (PRESENT, 'Present'), + (TARDY, 'Tardy'), + (ABSENT, 'Absent'), + ] + + school_day = models.ForeignKey(SchoolDay, on_delete=models.CASCADE) + student = models.ForeignKey(Student, on_delete=models.CASCADE) + status = models.CharField( + max_length=1, + choices=STATUS_CHOICES, + default=PRESENT + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.day} | {self.student} | {self.status}" + + class Meta: + verbose_name_plural = 'entries' + ordering = ['student'] diff --git a/src/core/templates/core/partials/pagination.html b/src/core/templates/core/partials/pagination.html new file mode 100644 index 0000000..8d33d89 --- /dev/null +++ b/src/core/templates/core/partials/pagination.html @@ -0,0 +1,17 @@ +
+ + {% if page_obj.has_previous %} + « first + previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next + last » + {% endif %} + +
diff --git a/src/core/templates/core/schoolyear_confirm_delete.html b/src/core/templates/core/schoolyear_confirm_delete.html new file mode 100644 index 0000000..b94a2ab --- /dev/null +++ b/src/core/templates/core/schoolyear_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +

Delete Schoolyear

+
+ {% csrf_token %} +

Are you sure you want to delete "{{ schoolyear }}"?

+ {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/src/core/templates/core/schoolyear_create_form.html b/src/core/templates/core/schoolyear_create_form.html new file mode 100644 index 0000000..49f457f --- /dev/null +++ b/src/core/templates/core/schoolyear_create_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

+ New Schoolyear

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/src/core/templates/core/schoolyear_detail.html b/src/core/templates/core/schoolyear_detail.html new file mode 100644 index 0000000..0a02ad2 --- /dev/null +++ b/src/core/templates/core/schoolyear_detail.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} + +{% block head_title %}Project {{ schoolyear.name }} | {% endblock head_title %} + +{% block content %} + +{% endblock %} diff --git a/src/core/templates/core/schoolyear_form.html b/src/core/templates/core/schoolyear_form.html new file mode 100644 index 0000000..2b3238c --- /dev/null +++ b/src/core/templates/core/schoolyear_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

Update Schoolyear

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/src/core/templates/core/schoolyear_list.html b/src/core/templates/core/schoolyear_list.html new file mode 100644 index 0000000..ca8583c --- /dev/null +++ b/src/core/templates/core/schoolyear_list.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% load static %} + +{% block head_title %}School Years | {% endblock head_title %} + +{% block content %} +
+
+
+

School Years

+ {% if perms.core.can_add_schoolyear %} + + New Year + {% endif %} +
+
+ + + + + + + + + + + + + {% for schoolyear in schoolyear_list %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
CreatedYearStudentsComponentsLast Updated
No schoolyears yet.
+ {% include 'core/partials/pagination.html' %} +
+ +{% endblock %} diff --git a/src/core/templates/core/student_confirm_delete.html b/src/core/templates/core/student_confirm_delete.html new file mode 100644 index 0000000..b6a8c00 --- /dev/null +++ b/src/core/templates/core/student_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +

Delete Student

+
+ {% csrf_token %} +

Are you sure you want to delete "{{ student }}"?

+ {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/src/core/templates/core/student_create_form.html b/src/core/templates/core/student_create_form.html new file mode 100644 index 0000000..b956282 --- /dev/null +++ b/src/core/templates/core/student_create_form.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block breadcrumbs %} + +{% endblock breadcrumbs %} + +{% block content %} +
+

+ New student

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+
+{% endblock %} diff --git a/src/core/templates/core/student_detail.html b/src/core/templates/core/student_detail.html new file mode 100644 index 0000000..906a91f --- /dev/null +++ b/src/core/templates/core/student_detail.html @@ -0,0 +1,131 @@ +{% extends 'base.html' %} +{% load helpers %} + +{% block head_title %}Student {{ student.student_id }} | {% endblock head_title %} + +{% block breadcrumbs %} + +{% endblock breadcrumbs %} + +{% block content %} +
+
+
+

{{ student.student_id }}   /   {{ student.full_name }}

+

Added:
+ Last updated:

+
+ Edit +
+
+
+ {% if student.allergies %} +
Allergies
+
{{ student.allergies }}
+ {% endif %} +
Birthday
+
{{ student.dob }}
+
Age
+
{{ student.age }}
+ {% if student.address %} +
Address
+
+
{{ student.address|linebreaksbr }}
+
+ {% endif %} +
+
+
+

Grades

+ + + + + + + {% for subject in subject_list %} + + + + + {% empty %} + + + + {% endfor %} + +
SubjectGrade
{{subject}}{{subject.grade|grade_as_percentage:subject.grade_total}}%
No grades yet. To add a grade you will need to enter a score for this student on a component.
+
+
+

Components

+ +
+ {% regroup score_list by component.subject as score_list %} + {% for subject in score_list %} +

{{subject.grouper}}

+ + + + + + + + + + + + + {% for score in subject.list %} + + + + + + + + + + {% endfor %} + +
Due DateComponentCategoryScoreTotalPercentage
{{score.component.due_date}}{{score.component}}{{score.component.get_category_display}}{{score.value}}{{score.component.grade_total}}{{score.grade_as_percentage}}%Change score
+ {% empty %} +

No components graded yet.

+ {% endfor %} +
+
+
+

Attendence

+ +
+ {% regroup entry_list by get_status_display as rentry_list %} + {% for status in rentry_list %} +

{{ status.grouper }}

+ + + + + + + + + {% for entry in status.list %} + + + + + {% endfor %} + +
DateStatus
{{entry.day.date}}Update
+ {% empty %} +

No attendence taken yet.

+ {% endfor %} +
+
+
+{% endblock %} diff --git a/src/core/templates/core/student_form.html b/src/core/templates/core/student_form.html new file mode 100644 index 0000000..534a797 --- /dev/null +++ b/src/core/templates/core/student_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

Update Student

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/src/core/templates/core/student_list.html b/src/core/templates/core/student_list.html new file mode 100644 index 0000000..d5e82d2 --- /dev/null +++ b/src/core/templates/core/student_list.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block head_title %}Students | {% endblock head_title %} + +{% block breadcrumbs %} + +{% endblock breadcrumbs %} + +{% block content %} +
+
+
+

Students

+ + New Student +
+ Student Tags → +
+ + + + + + + + + + {% for student in student_list %} + + + + + + {% empty %} + + + + {% endfor %} + +
Student No. ↕NameTags
No students yet.
+ {% include 'core/partials/pagination.html' %} +
+{% endblock %} diff --git a/src/core/templatetags/__init__.py b/src/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/templatetags/helpers.py b/src/core/templatetags/helpers.py new file mode 100644 index 0000000..00803d2 --- /dev/null +++ b/src/core/templatetags/helpers.py @@ -0,0 +1,16 @@ +from django import template + +register = template.Library() + + +@register.filter(name='grade_as_percentage') +def grade_as_percentage(numerator, denominator): + return round(numerator / denominator * 100, 2) + + +@register.filter(name='keyvalue') +def keyvalue(dict, key): + try: + return dict[key-1] + except KeyError: + return '' diff --git a/src/core/tests.py b/src/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/core/urls.py b/src/core/urls.py new file mode 100644 index 0000000..650e8fb --- /dev/null +++ b/src/core/urls.py @@ -0,0 +1,61 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + # SchoolYears + path('years/', include([ + path( + '', + views.SchoolYearListView.as_view(), + name='schoolyear-list' + ), + path( + 'new/', + views.SchoolYearCreateView.as_view(), + name='schoolyear-create' + ), + path('/', include([ + path( + '', + views.SchoolYearDetailView.as_view(), + name='schoolyear-detail' + ), + path( + 'update/', + views.SchoolYearUpdateView.as_view(), + name='schoolyear-update' + ), + + # Students + path('students/', include([ + path( + '', + views.StudentListView.as_view(), + name='student-list' + ), + path( + 'new/', + views.StudentCreateView.as_view(), + name='student-create' + ), + path('/', include([ + path( + '', + views.StudentDetailView.as_view(), + name='student-detail' + ), + path( + 'update/', + views.StudentUpdateView.as_view(), + name='student-update' + ), + path( + 'delete/', + views.StudentDeleteView.as_view(), + name='student-delete' + ), + ])), + ])), + ])), + ])), +] diff --git a/src/core/views.py b/src/core/views.py new file mode 100644 index 0000000..352f73e --- /dev/null +++ b/src/core/views.py @@ -0,0 +1,152 @@ +from django.conf import settings +from django.utils import timezone +from django.db import models +from django.shortcuts import render, reverse, redirect, get_object_or_404 +from django.urls import reverse, reverse_lazy +from django.views.generic.base import TemplateView +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from django.views.generic.dates import YearArchiveView +from django.views.generic.edit import ( + FormView, CreateView, UpdateView, DeleteView, FormMixin +) +from django.contrib import messages +from django.contrib.auth.mixins import ( + LoginRequiredMixin, PermissionRequiredMixin +) +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib.contenttypes.models import ContentType +from django.db.models import ( + Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value +) + +from .models import ( + SchoolYear, + StudentTag, + Student, + Subject, + Tag, + Component, + Score, + SchoolDay, + AttendanceEntry, +) + +from .forms import ( + SchoolYearCreateForm +) + + +class SchoolYearListView(ListView): + model = SchoolYear + + +class SchoolYearDetailView(DetailView): + model = SchoolYear + slug_url_kwarg = 'year' + slug_field = 'year' + + +class SchoolYearCreateView(SuccessMessageMixin, CreateView): + model = SchoolYear + success_message = 'SchoolYear created.' + form_class = SchoolYearCreateForm + template_name_suffix = '_create_form' + + +class SchoolYearUpdateView(SuccessMessageMixin, UpdateView): + model = SchoolYear + slug_url_kwarg = 'year' + slug_field = 'year' + success_message = 'SchoolYear saved.' + fields = '__all__' + + +class StudentListView(ListView): + model = Student + paginate_by = 50 + + def get_queryset(self): + queryset = Student.objects.filter( + school_year__year=self.kwargs['year'] + ) + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['school_year'] = get_object_or_404( + SchoolYear, year=self.kwargs['year'] + ) + return context + + +class StudentDetailView(DetailView): + model = Student + pk_url_kwarg = 'student_pk' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['score_list'] = Score.objects.select_related( + 'student' + ).prefetch_related( + 'component' + ).select_related( + 'component__subject' + ).filter( + student=self.object + ).order_by( + 'component__subject', + '-component__due_date' + ) + + context['subject_list'] = Subject.objects.filter( + component__score__student=self.object + ).annotate( + grade=Sum(F('component__score__value')), + grade_total=Sum('component__grade_total') + ) + + context['entry_list'] = AttendanceEntry.objects.select_related( + 'school_day' + ).filter( + student=self.object + ).order_by('status', '-school_day__date').select_related('student') + + return context + + +class StudentCreateView(SuccessMessageMixin, CreateView): + model = Student + success_message = 'Student created.' + template_name_suffix = '_create_form' + fields = [ + 'student_id', + 'first_name', + 'last_name', + 'address', + 'dob', + ] + + def form_valid(self, form): + form.instance.school_year = get_object_or_404(SchoolYear, year=self.kwargs['year']) + return super().form_valid(form) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['school_year'] = get_object_or_404(SchoolYear, year=self.kwargs['year']) + return context + + +class StudentUpdateView(SuccessMessageMixin, UpdateView): + model = Student + pk_url_kwarg = 'student_pk' + success_message = 'Student saved.' + fields = '__all__' + + +class StudentDeleteView(SuccessMessageMixin, DeleteView): + model = Student + pk_url_kwarg = 'student_pk' + success_message = 'Student deleted.' + success_url = reverse_lazy('student-list') diff --git a/src/gradebook/__init__.py b/src/gradebook/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gradebook/admin.py b/src/gradebook/admin.py new file mode 100644 index 0000000..8b2132d --- /dev/null +++ b/src/gradebook/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from .models import ( + Tag, + Subject, + Component, + Score, +) + +admin.site.register(Tag) +admin.site.register(Subject) +admin.site.register(Component) +admin.site.register(Score) diff --git a/src/gradebook/apps.py b/src/gradebook/apps.py new file mode 100644 index 0000000..02e5114 --- /dev/null +++ b/src/gradebook/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GradebookConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'gradebook' diff --git a/src/gradebook/forms.py b/src/gradebook/forms.py new file mode 100644 index 0000000..80928c8 --- /dev/null +++ b/src/gradebook/forms.py @@ -0,0 +1,43 @@ +from django import forms +from .models import Component, Score, Tag + +from students.models import Student + +class ComponentForm(forms.ModelForm): + class Meta: + model = Component + fields = ( + 'name', + 'category', + 'due_date', + 'grade_total', + ) + widgets = { + 'due_date': forms.DateInput(attrs = { + 'type': 'date' + }), + } + +class ComponentUpdateForm(forms.ModelForm): + class Meta: + model = Component + fields = ( + 'subject', + 'name', + 'category', + 'due_date', + 'grade_total', + 'tags', + ) + widgets = { + 'due_date': forms.DateInput(attrs = { + 'type': 'date' + }), + } + +class TagForm(forms.ModelForm): + class Meta: + model = Tag + fields = ( + 'name', + ) \ No newline at end of file diff --git a/src/gradebook/migrations/0001_initial.py b/src/gradebook/migrations/0001_initial.py new file mode 100644 index 0000000..1be9dec --- /dev/null +++ b/src/gradebook/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 3.2.7 on 2021-09-01 15:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('students', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Component', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('category', models.CharField(choices=[('QZ', 'Quiz'), ('AS', 'Assignment'), ('TS', 'Test')], default='AS', max_length=2)), + ('due_date', models.DateField()), + ('grade_total', models.PositiveIntegerField()), + ], + options={ + 'ordering': ['due_date'], + }, + ), + migrations.CreateModel( + name='Subject', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=250)), + ('description', models.CharField(blank=True, max_length=250)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ], + ), + migrations.CreateModel( + name='Score', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.PositiveIntegerField()), + ('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gradebook.component')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='students.student')), + ], + options={ + 'ordering': ('student',), + }, + ), + migrations.AddField( + model_name='component', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gradebook.subject'), + ), + migrations.AddField( + model_name='component', + name='tags', + field=models.ManyToManyField(blank=True, to='gradebook.Tag'), + ), + ] diff --git a/src/gradebook/migrations/0002_component_finished_grading.py b/src/gradebook/migrations/0002_component_finished_grading.py new file mode 100644 index 0000000..d0ee74f --- /dev/null +++ b/src/gradebook/migrations/0002_component_finished_grading.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.7 on 2021-09-16 23:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gradebook', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='component', + name='finished_grading', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/gradebook/migrations/__init__.py b/src/gradebook/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gradebook/models.py b/src/gradebook/models.py new file mode 100644 index 0000000..30eb050 --- /dev/null +++ b/src/gradebook/models.py @@ -0,0 +1,100 @@ +from datetime import datetime, date +from django.db import models +from django.urls import reverse + +from django.db.models import Count, Sum, Avg, F, Value +from django.db.models.functions import Length, Upper + +from students.models import Student + +class Tag(models.Model): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('tag-detail', kwargs={'pk': self.pk}) + + +class Subject(models.Model): + class Meta: + ordering = ['name'] + + name = models.CharField(max_length=250) + description = models.CharField(max_length=250, blank=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('subject-detail', kwargs={'pk': self.pk}) + + +class Component(models.Model): + class Meta: + ordering = ['due_date'] + + CATEGORY_CHOICES = [ + ('QZ', 'Quiz'), + ('AS', 'Assignment'), + ('TS', 'Test'), + ] + + subject = models.ForeignKey(Subject, on_delete=models.CASCADE) + name = models.CharField(max_length=50) + category = models.CharField( + max_length = 2, + choices = CATEGORY_CHOICES, + default='AS', + ) + due_date = models.DateField() + grade_total = models.PositiveIntegerField() + tags = models.ManyToManyField(Tag, blank=True) + finished_grading = models.BooleanField(default=False) + + @property + def is_due(self): + if self.due_date < date.today(): + return True + else: + return False + + @property + def grade_avg(self): + avg = Score.objects.filter(component=self.pk).aggregate( + Avg('value') + ) + if avg['value__avg'] is not None: + return round(avg['value__avg'], 2) + else: + return "No scores yet." + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('component-detail', kwargs={'pk': self.pk}) + + +class Score(models.Model): + class Meta: + ordering = ('student',) + + component = models.ForeignKey(Component, on_delete=models.CASCADE) + student = models.ForeignKey(Student, on_delete=models.CASCADE) + value = models.PositiveIntegerField() + + @property + def grade(self): + return f"{self.value} / {self.component.grade_total}" + + @property + def grade_as_percentage(self): + return round(self.value / self.component.grade_total * 100, 2) + + def __str__(self): + return f"{self.student} scored: {self.value} / {self.component.grade_total}" + + def get_absolute_url(self): + return reverse('score-detail', kwargs={'pk': self.pk}) diff --git a/src/gradebook/templates/gradebook/component_confirm_delete.html b/src/gradebook/templates/gradebook/component_confirm_delete.html new file mode 100644 index 0000000..1754832 --- /dev/null +++ b/src/gradebook/templates/gradebook/component_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{component}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/component_create_form.html b/src/gradebook/templates/gradebook/component_create_form.html new file mode 100644 index 0000000..0efac64 --- /dev/null +++ b/src/gradebook/templates/gradebook/component_create_form.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{subject}}

+

Create Component

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/component_detail.html b/src/gradebook/templates/gradebook/component_detail.html new file mode 100644 index 0000000..72e2888 --- /dev/null +++ b/src/gradebook/templates/gradebook/component_detail.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

{{component.name}}

+ Update Component +
+ {% if component.finished_grading %} +
+

+ âś“ Graded +

+
+ {% endif %} + {% if component.tags.count > 0 %} +
+ + {% for tag in component.tags.all %} + {{tag.name}} + {% endfor %} + +
+ {% endif %} +
+
+
Due Date
+
{{component.due_date}}
+
Description
+
{{component.name}}
+
Category
+
{{component.get_category_display}}
+
Grade Total
+
{{component.grade_total}}
+
+
+
+

Scores

+

+ Bulk Edit Scores +

+ + + + + + + + + {% for score in component.score_set.all %} + + + + + + {% endfor %} + + + + + +
StudentScore
{{score.student.student_id}} — {{score.student}}{{score.value}}Edit score
Avg Score{{component.grade_avg_pre|floatformat:2}}
+ + {% if scoreless %} +
+ +

Scoreless

+
    + {% for student in scoreless %} +
  • {{student}}
  • + {% endfor %} +
+
+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/component_form.html b/src/gradebook/templates/gradebook/component_form.html new file mode 100644 index 0000000..582da01 --- /dev/null +++ b/src/gradebook/templates/gradebook/component_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/component_manager.html b/src/gradebook/templates/gradebook/component_manager.html new file mode 100644 index 0000000..f37acfd --- /dev/null +++ b/src/gradebook/templates/gradebook/component_manager.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% load gradebook_filters %} + +{% block content %} +
+

{{component}}, {{component.subject}}

+
+
+ {% csrf_token %} +

Enter Scores

+ + + + + + + + + + {% if formset.errors %} + + {% endif %} + + + + {% for student in student_list %} + + + + + {% endfor %} + +
Out of:{{component.grade_total}}
StudentScoreErrors
{{student.full_name}}
+ {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/score_confirm_delete.html b/src/gradebook/templates/gradebook/score_confirm_delete.html new file mode 100644 index 0000000..252d0b6 --- /dev/null +++ b/src/gradebook/templates/gradebook/score_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{score}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/score_create_form.html b/src/gradebook/templates/gradebook/score_create_form.html new file mode 100644 index 0000000..688e005 --- /dev/null +++ b/src/gradebook/templates/gradebook/score_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Score

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/score_form.html b/src/gradebook/templates/gradebook/score_form.html new file mode 100644 index 0000000..b1a3a01 --- /dev/null +++ b/src/gradebook/templates/gradebook/score_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} +
+

Update Score

+

{{score.component.subject}}— {{score.component.get_category_display}}: {{score.component}}

+
+
+ {% csrf_token %} + +

{{score.student}}

+ {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/search_results.html b/src/gradebook/templates/gradebook/search_results.html new file mode 100644 index 0000000..8c3de8e --- /dev/null +++ b/src/gradebook/templates/gradebook/search_results.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +
+

Search Results

+ {% if student_list.count > 0 %} +
+

Students

+
    + {% for student in student_list %} +
  • + {{student.student_id}} — {{student.full_name}} + {% if student.sit %} + SIT: {{student.get_sit_display}} + {% endif %} + {% if student.iep_behavioral %} + IEP behavioral + {% endif %} + {% if student.iep_math %} + IEP math + {% endif %} + {% if student.iep_ela %} + IEP ELA + {% endif %} + {% if student.parent__count > 0 %} +
    Parents: + {% for parent in student.parent_set.all %} + {{parent.full_name}}{% if not forloop.last %},{% endif %} + {% endfor %} + {% endif %} +
  • + {% endfor %} +
+
+ {% endif %} + {% if component_list.count > 0 %} +
+

Components

+
    +
  • + Subject + Component + Due Date + Tags +
  • +
    + {% for component in component_list %} +
  • + {{component.subject}} + {{component}} + {{component.due_date}} + + {% for tag in component.tags.all %} + {{tag.name}} + {% endfor %} + +
  • + {% empty %} +

    No components by that name were found.

    + {% endfor %} +
+
+ {% endif %} + {% if message_list.count > 0 %} +
+

Messages

+
    + {% for message in message_list %} +
  • +

    + {{message.thread.subject}}
    + {{message.content|truncatewords:25}}
    + Read more → +

    +
  • + {% empty %} +

    No components by that name were found.

    + {% endfor %} +
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/subject_confirm_delete.html b/src/gradebook/templates/gradebook/subject_confirm_delete.html new file mode 100644 index 0000000..d7fc822 --- /dev/null +++ b/src/gradebook/templates/gradebook/subject_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{subject}} Subject

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/subject_create_form.html b/src/gradebook/templates/gradebook/subject_create_form.html new file mode 100644 index 0000000..1ca81c3 --- /dev/null +++ b/src/gradebook/templates/gradebook/subject_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Subject

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/subject_detail.html b/src/gradebook/templates/gradebook/subject_detail.html new file mode 100644 index 0000000..09f11bf --- /dev/null +++ b/src/gradebook/templates/gradebook/subject_detail.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

{{subject.name}}

+ Update subject details +
+ {% if subject.description %} + {{subject.description}} + {% endif %} +
+

Syllabus

+

+ + New component +

+ + + + + + + + + + + + {% for component in subject.component_set.all %} + + + + + + + + {% endfor %} + +
Due DateDescriptionCategoryGrade TotalAvg Score
{{component.due_date}}{{component.name}}{{component.get_category_display}}{{component.grade_total}}{{component.grade_avg_pre|floatformat:2}}
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/subject_form.html b/src/gradebook/templates/gradebook/subject_form.html new file mode 100644 index 0000000..8003f89 --- /dev/null +++ b/src/gradebook/templates/gradebook/subject_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/subject_list.html b/src/gradebook/templates/gradebook/subject_list.html new file mode 100644 index 0000000..64ace4a --- /dev/null +++ b/src/gradebook/templates/gradebook/subject_list.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Curricula

+
+
+

Subjects

+

+ + New subject +

+ {% for subject in subject_list %} +
+

{{subject.name}}

+ {% if subject.description %} +

{{subject.description}}

+ {% endif %} +
+ {% endfor %} +
+
+

Tags

+

+ New tag

+

+ {% for tag in tag_list %} + + {{tag.name}} + {{tag.component__count}} + + {% endfor %} +

+
+
+{% endblock %} diff --git a/src/gradebook/templates/gradebook/tag_confirm_delete.html b/src/gradebook/templates/gradebook/tag_confirm_delete.html new file mode 100644 index 0000000..c6d8033 --- /dev/null +++ b/src/gradebook/templates/gradebook/tag_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{tag}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/tag_create_form.html b/src/gradebook/templates/gradebook/tag_create_form.html new file mode 100644 index 0000000..e325178 --- /dev/null +++ b/src/gradebook/templates/gradebook/tag_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Tag

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/tag_detail.html b/src/gradebook/templates/gradebook/tag_detail.html new file mode 100644 index 0000000..0cde70c --- /dev/null +++ b/src/gradebook/templates/gradebook/tag_detail.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

{{tag.name}}

+ Update tag details +
+
+

Components with this tag

+ + + + + + + + + + + + {% for component in tag.component_set.all %} + + + + + + + + {% endfor %} + +
Due DateDescriptionCategoryGrade TotalAvg Score
{{component.due_date}}{{component.subject}}: {{component.name}}{{component.get_category_display}}{{component.grade_total}}{{component.grade_avg_pre|floatformat:2}}
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/tag_form.html b/src/gradebook/templates/gradebook/tag_form.html new file mode 100644 index 0000000..8c30362 --- /dev/null +++ b/src/gradebook/templates/gradebook/tag_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Update Tag

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templates/gradebook/tag_list.html b/src/gradebook/templates/gradebook/tag_list.html new file mode 100644 index 0000000..99fab71 --- /dev/null +++ b/src/gradebook/templates/gradebook/tag_list.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
+

Tags

+

+ + New tag +

+
+
    + {% for tag in tag_list %} +
  • + {{tag.name}} + {{tag.component__count}} +
  • + {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/gradebook/templatetags/__init__.py b/src/gradebook/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gradebook/templatetags/gradebook_filters.py b/src/gradebook/templatetags/gradebook_filters.py new file mode 100644 index 0000000..3c02455 --- /dev/null +++ b/src/gradebook/templatetags/gradebook_filters.py @@ -0,0 +1,14 @@ +from django import template + +register = template.Library() + +@register.filter(name='grade_as_percentage') +def grade_as_percentage(numerator, denominator): + return round(numerator / denominator * 100, 2) + +@register.filter(name='keyvalue') +def keyvalue(dict, key): + try: + return dict[key-1] + except KeyError: + return '' diff --git a/src/gradebook/tests.py b/src/gradebook/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/gradebook/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/gradebook/urls.py b/src/gradebook/urls.py new file mode 100644 index 0000000..4d5cf68 --- /dev/null +++ b/src/gradebook/urls.py @@ -0,0 +1,40 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + path('search/', views.SearchResultsView.as_view(), name='search-results'), + path('subjects/', views.SubjectListView.as_view(), name='subject-list'), + path('subjects/new/', views.SubjectCreateView.as_view(), name='subject-create'), + path('subjects//', include([ + path('', views.SubjectDetailView.as_view(), name='subject-detail'), + path('update/', views.SubjectUpdateView.as_view(), name='subject-update'), + path('delete/', views.SubjectDeleteView.as_view(), name='subject-delete'), + + + path('components/', views.ComponentListView.as_view(), name='component-list'), + path('components/new/', views.ComponentCreateView.as_view(), name='component-create'), + path('components//', include([ + path('', views.ComponentDetailView.as_view(), name='component-detail'), + path('update/', views.ComponentUpdateView.as_view(), name='component-update'), + path('manager/', views.ComponentManagerView.as_view(), name='component-manager'), + path('delete/', views.ComponentDeleteView.as_view(), name='component-delete'), + ])), + ])), + + + path('scores/', views.ScoreListView.as_view(), name='score-list'), + path('scores/new/', views.ScoreCreateView.as_view(), name='score-create'), + path('scores//', include([ + path('', views.ScoreDetailView.as_view(), name='score-detail'), + path('update/', views.ScoreUpdateView.as_view(), name='score-update'), + path('delete/', views.ScoreDeleteView.as_view(), name='score-delete'), + ])), + + path('tags/', views.TagListView.as_view(), name='tag-list'), + path('tags/new/', views.TagCreateView.as_view(), name='tag-create'), + path('tags//', include([ + path('', views.TagDetailView.as_view(), name='tag-detail'), + path('update/', views.TagUpdateView.as_view(), name='tag-update'), + path('delete/', views.TagDeleteView.as_view(), name='tag-delete'), + ])), +] diff --git a/src/gradebook/views.py b/src/gradebook/views.py new file mode 100644 index 0000000..a569bbe --- /dev/null +++ b/src/gradebook/views.py @@ -0,0 +1,297 @@ +from django.shortcuts import render +from django.urls import reverse_lazy, reverse +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.forms.models import inlineformset_factory +from django import forms + +from django.db.models import ( Exists, OuterRef, + Prefetch, Subquery, Count, Sum, Avg, F, Q, Value) +from django.db.models.functions import Length, Upper + +from students.models import Student, Thread, Message + +from .models import ( + Tag, + Subject, + Component, + Score, +) + +from .forms import ComponentForm, ComponentUpdateForm + + +# UPLOAD CSV +# import pandas as pd + +# csv_data = pd.read_csv('file.csv', sep=';') + +# products = [ +# Product( +# name = csv_data.ix[row]['Name'], +# description = csv_data.ix[row]['Description'], +# price = csv_data.ix[row]['price'], +# ) +# for row in csv_data['ID'] +# ] + +# Product.objects.bulk_create(products) + + +# if form.is_valid(): +# csv_file = form.cleaned_data['csv_file'] + + + +class SearchResultsView(ListView): + model = Component + template_name = 'gradebook/search_results.html' + + def get_queryset(self): + query = self.request.GET.get('q') + object_list = Component.objects.filter( + Q(name__icontains=query) | Q(tags__name__icontains=query) + ).prefetch_related( + Prefetch( + 'score_set', + queryset=Score.objects.select_related('student') + ), + 'tags' + ).annotate( + grade_avg_pre=Avg('score__value') + ).order_by('subject') + return object_list + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + query = self.request.GET.get('q') + context['student_list'] = Student.objects.filter( + Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(student_id__icontains=query) + ) + context['message_list'] = Message.objects.filter( + Q(content__icontains=query) + ) + context['query'] = query + return context + + +# SUBJECTS +class SubjectListView(LoginRequiredMixin, ListView): + model = Subject + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['tag_list'] = Tag.objects.annotate(Count('component')) + return context + +class SubjectCreateView(LoginRequiredMixin, CreateView): + model = Subject + template_name_suffix = '_create_form' + fields = ('__all__') + +class SubjectDetailView(LoginRequiredMixin, DetailView): + model = Subject + + def get_object(self): + queryset = Subject.objects.filter( + pk=self.kwargs.get(self.pk_url_kwarg) + ).prefetch_related( + Prefetch('component_set', queryset=Component.objects.prefetch_related( + 'score_set' + ).annotate( + grade_avg_pre=Avg('score__value') + )) + ) + obj = queryset.get() + return obj + + +class SubjectUpdateView(LoginRequiredMixin, UpdateView): + model = Subject + fields = ('__all__') + + def get_success_url(self): + pk = self.kwargs["pk"] + return reverse('subject-detail', kwargs={'pk': pk}) + +class SubjectDeleteView(LoginRequiredMixin, DeleteView): + model = Subject + success_url = reverse_lazy('subject-list') + + +# COMPONENTS +class ComponentListView(LoginRequiredMixin, ListView): + model = Component + pk_url_kwarg = 'comp_pk' + +class ComponentCreateView(LoginRequiredMixin, CreateView): + model = Component + form_class = ComponentForm + template_name_suffix = '_create_form' + pk_url_kwarg = 'comp_pk' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subject'] = Subject.objects.get(pk=self.kwargs['pk']) + return context + + def form_valid(self, form): + form.instance.subject = Subject.objects.get(pk=self.kwargs['pk']) + return super().form_valid(form) + + def get_success_url(self): + return reverse('component-detail', kwargs={'pk': self.kwargs['pk'], 'comp_pk': self.object.pk}) + +class ComponentDetailView(LoginRequiredMixin, DetailView): + model = Component + pk_url_kwarg = 'comp_pk' + + def get_object(self): + queryset = Component.objects.filter( + pk=self.kwargs.get(self.pk_url_kwarg) + ).prefetch_related( + Prefetch( + 'score_set', + queryset=Score.objects.select_related('student') + ), + 'tags' + ).annotate( + grade_avg_pre=Avg('score__value') + ) + obj = queryset.get() + return obj + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + cscores = Score.objects.filter( + component=self.object, + student=OuterRef('pk') + ) + context['scoreless'] = Student.objects.exclude( + score__in=cscores + ) + return context + +class ComponentUpdateView(LoginRequiredMixin, UpdateView): + model = Component + form_class = ComponentUpdateForm + pk_url_kwarg = 'comp_pk' + + def get_success_url(self): + return reverse('subject-detail', kwargs={'pk': self.object.subject.pk}) + +class ComponentManagerView(LoginRequiredMixin, UpdateView): + model = Component + fields = ('finished_grading',) + pk_url_kwarg = 'comp_pk' + template_name_suffix = '_manager' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + cscores = Score.objects.filter( + component=self.object, + student=OuterRef('pk') + ) + context['student_list'] = Student.objects.annotate( + cscore=Subquery(cscores.values('value')), + cscore_pk=Subquery(cscores.values('pk')) + ) + return context + + def get_success_url(self): + return reverse('component-detail', kwargs={'pk': self.object.subject.pk, 'comp_pk': self.object.pk}) + + def form_valid(self, form): + form.save() + for key, value in self.request.POST.items(): + if 'student' in key and value: + s_pk = key.split('_')[1] + obj, created = Score.objects.update_or_create( + component=self.object, + student=Student.objects.get(pk=s_pk), + defaults={'value': value} + ) + return super().form_valid(form) + +class ComponentDeleteView(LoginRequiredMixin, DeleteView): + model = Component + pk_url_kwarg = 'comp_pk' + + def get_success_url(self): + return reverse('subject-detail', kwargs={'pk': self.kwargs['pk']}) + + +# SCORES +class ScoreListView(LoginRequiredMixin, ListView): + model = Score + +class ScoreCreateView(LoginRequiredMixin, CreateView): + model = Score + template_name_suffix = '_create_form' + fields = ('__all__') + + def get_success_url(self): + return reverse('component-detail', kwargs={'pk': self.object.component.pk}) + +class ScoreDetailView(LoginRequiredMixin, DetailView): + model = Score + +class ScoreUpdateView(LoginRequiredMixin, UpdateView): + model = Score + fields = ['value'] + + def get_success_url(self): + return reverse('component-detail', kwargs={'pk': self.object.component.subject.pk, 'comp_pk': self.object.component.pk}) + +class ScoreDeleteView(LoginRequiredMixin, DeleteView): + model = Score + + def get_success_url(self): + return reverse('component-detail', kwargs={'pk': self.object.component.subject.pk, 'comp_pk': self.object.component.pk}) + + + +# TAGS +class TagListView(LoginRequiredMixin, ListView): + model = Tag + + def get_queryset(self): + object_list = Tag.objects.annotate(Count('component')) + return object_list + +class TagCreateView(LoginRequiredMixin, CreateView): + model = Tag + template_name_suffix = '_create_form' + fields = ('__all__') + +class TagDetailView(LoginRequiredMixin, DetailView): + model = Tag + + def get_object(self): + queryset = Tag.objects.filter( + pk=self.kwargs.get(self.pk_url_kwarg) + ).prefetch_related( + Prefetch('component_set', queryset=Component.objects.prefetch_related( + 'score_set' + ).annotate( + grade_avg_pre=Avg('score__value') + )) + ) + obj = queryset.get() + return obj + + +class TagUpdateView(LoginRequiredMixin, UpdateView): + model = Tag + fields = ('__all__') + + def get_success_url(self): + pk = self.kwargs["pk"] + return reverse('tag-detail', kwargs={'pk': pk}) + +class TagDeleteView(LoginRequiredMixin, DeleteView): + model = Tag + success_url = reverse_lazy('tag-list') diff --git a/src/indici/asgi.py b/src/indici/asgi.py new file mode 100644 index 0000000..9fe97a1 --- /dev/null +++ b/src/indici/asgi.py @@ -0,0 +1,11 @@ +""" +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'indici.settings') + +application = get_asgi_application() diff --git a/src/indici/config.py b/src/indici/config.py new file mode 100644 index 0000000..211a768 --- /dev/null +++ b/src/indici/config.py @@ -0,0 +1,20 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +DEBUG = os.environ.get('DEBUG', 'True') == 'True' + +DATABASE_CONFIG = { + 'port': os.environ.get('DATABASE_PORT', ''), + 'host': os.environ.get('DATABASE_HOST', ''), + 'password': os.environ.get('DATABASE_PASSWORD', ''), + 'user': os.environ.get('DATABASE_USER', ''), + 'name': os.environ.get('DATABASE_NAME', ''), + 'engine': os.environ.get('DATABASE_ENGINE', ''), +} +SECRET_KEY = os.environ.get('SECRET_KEY', '') +CACHE_CONFIG = { + 'location': os.environ.get('CACHE_LOCATION', ''), + 'backend': os.environ.get('CACHE_BACKEND', ''), +} diff --git a/src/indici/middleware.py b/src/indici/middleware.py new file mode 100644 index 0000000..3a01cd2 --- /dev/null +++ b/src/indici/middleware.py @@ -0,0 +1,16 @@ +import zoneinfo +from django.utils import timezone + + +class TimezoneMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + tzname = request.COOKIES.get('timezone') + if tzname: + tzname = parse.unquote(tzname) + timezone.activate(zoneinfo.ZoneInfo(tzname)) + else: + timezone.deactivate() + return self.get_response(request) diff --git a/src/indici/settings.py b/src/indici/settings.py new file mode 100644 index 0000000..3b7ea10 --- /dev/null +++ b/src/indici/settings.py @@ -0,0 +1,153 @@ +import os +from pathlib import Path +from django.urls import reverse_lazy + +from .config import * + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Add Your Required Allow Host +ALLOWED_HOSTS = [] + +INTERNAL_IPS = [ + '127.0.0.1', + 'localhost', +] + + +# Application definition +INSTALLED_APPS = [ + # Django + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # 3rd Party + 'debug_toolbar', + 'django_filters', + 'compressor', + + # Local + 'accounts.apps.AccountsConfig', + 'core.apps.CoreConfig', + 'students.apps.StudentsConfig', + 'gradebook.apps.GradebookConfig', + 'attendance.apps.AttendanceConfig', +] + +# Middlewares +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', + 'indici.middleware.TimezoneMiddleware', +] + +ROOT_URLCONF = 'indici.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / "templates"], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'indici.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +CACHE = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': 'redis://127.0.0.1:6379', + } +} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' +if DEBUG: + STATIC_ROOT = BASE_DIR / 'public' +else: + STATIC_ROOT = STATIC_ROOT_PATH +STATICFILES_DIRS = [BASE_DIR / 'static'] +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', +) + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = 'accounts.User' +LOGIN_REDIRECT_URL = reverse_lazy('student-list') + +# Decimal settings +DEFAULT_DECIMAL_PLACES = 2 +DEFAULT_MAX_DIGITS = 12 diff --git a/src/indici/urls.py b/src/indici/urls.py new file mode 100644 index 0000000..d4ac9ff --- /dev/null +++ b/src/indici/urls.py @@ -0,0 +1,19 @@ +from django.conf import settings +from django.contrib import admin +from django.urls import path, include +from django.conf.urls.static import static + +urlpatterns = [ + path('', include(('core.urls', 'core'), namespace='core')), + path('accounts/', include('accounts.urls'), name='accounts'), + path('accounts/', include('django.contrib.auth.urls')), + path('students/', include('students.urls'), name='students'), + path('gradebook/', include('gradebook.urls'), name='gradebook'), + path('attendance/', include('attendance.urls'), name='attendance'), + path('admin/', admin.site.urls), + path('__debug__/', include('debug_toolbar.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/src/indici/wsgi.py b/src/indici/wsgi.py new file mode 100644 index 0000000..95e7063 --- /dev/null +++ b/src/indici/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'indici.settings') + +application = get_wsgi_application() diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..2ed2253 --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'indici.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/src/static/styles/main.css b/src/static/styles/main.css new file mode 100644 index 0000000..c825ea9 --- /dev/null +++ b/src/static/styles/main.css @@ -0,0 +1,469 @@ +:root { + /* --bg: #e6e6e6; + --border-color: #dfdfdf; + --blue: #106f8c; + --green: #0F9B2B; + --red: #EC1111; + --light-brown: #a69688;*/ + + /* Colors */ + --color-bg-dark: #2c3b3e; + --color-bg-light: #fdead1; + --shadow-color: #bfbfbf; + --white: #ffffff; + --link-color: #2c6e95; + --color-yellow: #f9e476; + --color-light-gray: #e3e3e3; + --color-green: #2c953c; + --color-blue: #2c6e95; + + /* Helper colors */ + --color-muted: #eeeeee; + --color-primary: black; + --color-success: var(--color-green); + --color-info: #eeeeee; + --color-warning: orange; + --color-danger: #d21e1e; + + /* Defaults */ + --table-border: 0.0125rem solid black; + --default-border: 0.0125rem solid #2c3b3e; +} + +html { + font-size: 100%; +} + +body { + margin: 0; + padding: 0; + background-color: var(--color-bg-light); + font-family: 'Lato', sans-serif; + font-weight: 400; + line-height: 1; + color: black; +} +p { + margin-top: 0; + margin-bottom: 1rem; +} +a { + color: var(--link-color); + cursor: pointer; +} +h1, h2, h3, h4, h5 { + margin: 0 0 1rem; + font-family: 'Lato', sans-serif; + font-weight: 900; + line-height: 1; + /*color: var(--color-bg-dark);*/ +} +h1 { + font-size: 1.802rem; +} +h2 { + font-size: 1.602rem; +} +h3 { + font-size: 1.424rem; +} +h4 { + font-size: 1.266rem; +} +h5 { + font-size: 1.125rem; +} +small { + font-size: 0.889rem; +} + + +/* Blockquote + ========================================================================== */ +blockquote { + margin: 0; +} + +blockquote p { + padding: 15px; + background: var(--color-muted); + border-radius: 0.5rem; + line-height: 1.75; + font-size: 1.25rem; +} + + +/* Figure + ========================================================================== */ +figure { + padding: 0; + margin: 0; +} + + + +/* Lists + ========================================================================== */ +ul, ol, dl { + margin-bottom: 1rem; +} +dl dt { + font-weight: bold; + +} + + +/* ========================================================================== + Base + ========================================================================== */ + + + +/* Tables + ========================================================================== */ +table { + border-collapse: collapse; + width: 100%; + text-align: left; + border-bottom: var(--table-border); +} +thead { + background-color: var(--color-blue); + color: var(--white); + border-bottom: var(--table-border); +} +thead a { + color: inherit; + text-decoration: none; +} +tr:nth-child(even) { + background-color: var(--color-light-gray); +} +th, td { + padding: 0.5rem 0.5rem; +} +tr { + /*border-bottom: var(--table-border);*/ +} +@media screen and (max-width: 600px) { + tr { + display: grid; + grid-template-columns: 1fr; + } +} +tbody tr:hover { + background-color: var(--color-yellow); +} + +tbody tr.has-link { + cursor: pointer; +} + +/* Forms + ========================================================================== */ +form { + margin: 0; + padding: 0; +} + +textarea { + box-sizing: border-box; + border: var(--default-border); + font: inherit; + font-size: 1.25rem; + padding: 0.5rem 1rem; + outline-color: var(--link-color); + width: 100%; + resize: vertical; + line-height: 1.75; + border-radius: 0.5rem; +} + +input[type=search] { + /*border: var(--default-border);*/ + border: none; + font: inherit; + padding: 0.25rem 0.5rem; + outline-color: var(--link-color); + +} +input[type=search]:focus { + background-color: var(--color-yellow); + border-color: var(--link-color); +} + +::placeholder { + font: inherit; + font-style: italic; +} + +button, +.action-button { + cursor: pointer; + border: none; + font: inherit; + background-color: var(--color-blue); + color: var(--white); + text-decoration: none; + padding: 0.5rem; + font-weight: bold; + box-sizing: border-box; + border-radius: 0.5rem; +} + +form progress { + display: none; +} + + +/* Messages + ========================================================================== */ +.messages { + position: fixed; + bottom: 1rem; + right: 1rem; + padding: 1rem; + background-color: var(--color-success); + color: white; + font-weight: bold; +} + +.messages p:last-child { + margin-bottom: 0; +} + + + + +/* Tag + ========================================================================== */ +.tag { + font-weight: bolder; + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; +} + +.tag__muted { + background-color: var(--color-muted); +} + +.tag__primary { + background-color: var(--color-primary); +} + +.tag__success { + background-color: var(--color-success); + color: white; +} + +.tag__info { + background-color: var(--color-info); +} + +.tag__warning { + background-color: var(--color-warning); +} + +.tag__danger { + background-color: var(--color-danger); + color: white; +} + +.text__muted { + font-weight: bold; + color: var(--color-muted); +} +.text__primary { + font-weight: bold; + color: var(--color-primary); +} +.text__success { + font-weight: bold; + color: var(--color-success); +} +.text__info { + font-weight: bold; + color: var(--color-info); +} +.text__warning { + font-weight: bold; + color: var(--color-warning); +} +.text__danger { + font-weight: bold; + color: var(--color-danger); +} + + +/* ========================================================================== + Header + ========================================================================== */ + +.site__header { + margin-bottom: 1rem; + background-color: var(--color-blue); + padding: 1rem; + color: var(--white); +} +.site__header a { + color: var(--white); + text-transform: lowercase; + font-variant: small-caps; + text-decoration: none; +} + +/* Site Nav + ========================================================================== */ +.site__nav { + display: grid; + align-items: center; + grid-template-columns: 1fr auto 1fr; + gap: 1rem; +} +.site__nav form { + /*justify-self: end;*/ +} +.site__nav menu { + margin: 0; + padding: 0; + list-style: none; + display: flex; +} +.site__nav menu li:not(:last-child) { + margin-right: 1rem; +} + +.site__nav a { + font-weight: bold; +} + +.nav__account { + justify-self: end; + /*align-items: center;*/ +} + +/* ========================================================================== + Main + ========================================================================== */ +main { + margin: 0 auto; + padding: 1rem 4rem 4rem; +} +@media screen and (max-width: 800px) { + main { + padding: 1rem; + } +} + +/* Article + ========================================================================== */ +article { + background-color: var(--white); + padding: 1.5rem; + /*box-shadow: 0 0 0.5rem lightgray;*/ +} + +article > header { + margin-bottom: 1.5rem; +} + +/* List + ========================================================================== */ + +.list__header { + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.list__title { + display: flex; + align-items: baseline; +} + +.list__title *:first-child { + margin-right: 1rem; +} + +/* Detail + ========================================================================== */ + + +.detail__header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.detail__info { + margin-bottom: 2rem; +} + +.detail__datalist { + display: grid; + grid-template-columns: auto 1fr; + gap: 1.3rem 1rem; +} + +.detail__datalist dt { + /*justify-self: end;*/ +} + +/* Breadcrumbs + ========================================================================== */ +.breadcrumbs { + padding: 0.25rem; + margin: 0 2.5rem; + background-color: var(--color-light-gray); +} +.breadcrumbs menu { + margin: 0; + padding: 0 1rem; + line-height: 1.75; + list-style: none; + display: flex; + /*justify-content: center;*/ + flex-wrap: wrap; +} +.breadcrumbs menu li:not(:last-child), +.breadcrumbs menu span:not(:last-child) { + margin-right: 0.5rem; +} + +/* Pagination + ========================================================================== */ +.pagination { + text-align: center; + margin-top: 1rem; +} + + +/* ========================================================================== + Tools + ========================================================================== */ +.tools { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.tool { + display: grid; + grid-template-rows: repeat(3, auto); + min-height: 8rem; + border: var(--default-border); + padding: 1rem; + border-radius: 0.5rem; + text-decoration: none; + color: var(--color-primary); +} + +.tool:hover { + background-color: var(--color-yellow); +} + +.tool *:last-child { + justify-self: end; + align-self: end; + font-size: 2rem; +} diff --git a/src/static/styles/normalize.css b/src/static/styles/normalize.css new file mode 100644 index 0000000..9d9f37a --- /dev/null +++ b/src/static/styles/normalize.css @@ -0,0 +1,350 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} + diff --git a/src/students/__init__.py b/src/students/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/students/admin.py b/src/students/admin.py new file mode 100644 index 0000000..a658b11 --- /dev/null +++ b/src/students/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import ( + Student, + Parent, +) + +admin.site.register(Student) +admin.site.register(Parent) diff --git a/src/students/apps.py b/src/students/apps.py new file mode 100644 index 0000000..c242c4d --- /dev/null +++ b/src/students/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StudentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'students' diff --git a/src/students/forms.py b/src/students/forms.py new file mode 100644 index 0000000..300750f --- /dev/null +++ b/src/students/forms.py @@ -0,0 +1,77 @@ +from django import forms +from django.utils import timezone + +from .models import Student, Parent, Thread, Message + +class StudentForm(forms.ModelForm): + class Meta: + model = Student + fields = ( + 'student_id', + 'first_name', + 'last_name', + 'address', + 'dob', + 'glasses', + 'allergies', + 'sit', + 'iep_behavioral', + 'iep_math', + 'iep_ela', + ) + labels = { + 'student_id': 'Student ID', + 'dob': 'DOB', + 'sit': 'SIT', + 'iep_behavioral': 'IEP Behavioral', + 'iep_math': 'IEP Math', + 'iep_ela': 'IEP ELA', + } + +class ParentForm(forms.ModelForm): + class Meta: + model = Parent + fields = ( + 'students', + 'first_name', + 'last_name', + 'relation', + 'phone_number', + 'email_address', + 'notes', + ) + +class ThreadForm(forms.ModelForm): + class Meta: + model = Thread + fields = ( + 'parent', + 'student', + 'subject', + ) + widgets = { + 'subject': forms.TextInput(attrs = { + 'placeholder': 'Reason for contact…' + }), + } + +class MessageForm(forms.ModelForm): + class Meta: + model = Message + fields = ( + 'method_of_contact', + 'date', + 'time', + 'content', + ) + widgets = { + 'content': forms.Textarea(attrs = { + 'placeholder': 'Enter your message here' + }), + 'date': forms.DateInput(attrs = { + 'type': 'date', + }), + 'time': forms.DateInput(attrs = { + 'type': 'time', + }), + } \ No newline at end of file diff --git a/src/students/migrations/0001_initial.py b/src/students/migrations/0001_initial.py new file mode 100644 index 0000000..1342781 --- /dev/null +++ b/src/students/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# Generated by Django 3.2.7 on 2021-09-01 15:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Parent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('relation', models.CharField(choices=[('MO', 'Mother'), ('FA', 'Father'), ('GM', 'Grandmother'), ('GP', 'Grandfather'), ('SM', 'Stepmother'), ('SF', 'Stepfather'), ('OT', 'Other')], default='MO', max_length=2)), + ('phone_number', models.IntegerField(blank=True, null=True)), + ('email_address', models.EmailField(blank=True, max_length=254, null=True)), + ('notes', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='Student', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('student_id', models.IntegerField()), + ('first_name', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('address', models.TextField(blank=True)), + ('dob', models.DateField()), + ('sit', models.CharField(blank=True, choices=[('T2', 'Tier 2'), ('T3', 'Tier 3')], max_length=2)), + ('iep_behavioral', models.BooleanField(default=False)), + ('iep_math', models.BooleanField(default=False)), + ('iep_ela', models.BooleanField(default=False)), + ], + options={ + 'ordering': ['first_name'], + }, + ), + migrations.CreateModel( + name='Thread', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=250)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='students.parent')), + ('student', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='students.student')), + ], + ), + migrations.AddField( + model_name='parent', + name='students', + field=models.ManyToManyField(blank=True, to='students.Student'), + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(blank=True, null=True)), + ('time', models.TimeField(blank=True, null=True)), + ('content', models.TextField()), + ('method_of_contact', models.CharField(blank=True, choices=[('PH', 'Phone'), ('EM', 'Email'), ('NO', 'Note'), ('IP', 'In person')], max_length=2)), + ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='students.thread')), + ], + options={ + 'ordering': ('-date',), + }, + ), + ] diff --git a/src/students/migrations/0002_auto_20210912_0003.py b/src/students/migrations/0002_auto_20210912_0003.py new file mode 100644 index 0000000..dc759a5 --- /dev/null +++ b/src/students/migrations/0002_auto_20210912_0003.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2021-09-12 00:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='allergies', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='student', + name='glasses', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/students/migrations/0003_alter_student_options.py b/src/students/migrations/0003_alter_student_options.py new file mode 100644 index 0000000..762c594 --- /dev/null +++ b/src/students/migrations/0003_alter_student_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.7 on 2021-09-12 00:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0002_auto_20210912_0003'), + ] + + operations = [ + migrations.AlterModelOptions( + name='student', + options={'ordering': ['student_id']}, + ), + ] diff --git a/src/students/migrations/0004_auto_20210912_0030.py b/src/students/migrations/0004_auto_20210912_0030.py new file mode 100644 index 0000000..4c80d1f --- /dev/null +++ b/src/students/migrations/0004_auto_20210912_0030.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2021-09-12 00:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('students', '0003_alter_student_options'), + ] + + operations = [ + migrations.AlterField( + model_name='parent', + name='first_name', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='parent', + name='last_name', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/src/students/migrations/__init__.py b/src/students/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/students/models.py b/src/students/models.py new file mode 100644 index 0000000..c37b855 --- /dev/null +++ b/src/students/models.py @@ -0,0 +1,131 @@ +from datetime import datetime, date +from django.db import models +from django.urls import reverse + +from django.db.models import Count, Sum, F, Value +from django.db.models.functions import Length, Upper + + +class Student(models.Model): + class Meta: + ordering = ['student_id'] + + student_id = models.IntegerField() + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + address = models.TextField(blank=True) + dob = models.DateField() + + T2 = 'T2' + T3 = 'T3' + SIT_CHOICES = [ + (T2, 'Tier 2'), + (T3, 'Tier 3'), + ] + + sit = models.CharField( + max_length=2, + choices=SIT_CHOICES, + blank=True, + ) + + glasses = models.BooleanField(default=False) + allergies = models.CharField(max_length=50, blank=True) + + iep_behavioral = models.BooleanField(default=False) + iep_math = models.BooleanField(default=False) + iep_ela = models.BooleanField(default=False) + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + @property + def age(self): + today = date.today() + return today.year - self.dob.year - ((today.month, today.day) < (self.dob.month, self.dob.day)) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + def get_absolute_url(self): + return reverse('student-detail', kwargs={'pk': self.pk}) + + +class Parent(models.Model): + RELATION_CHOICES = [ + ('MO', 'Mother'), + ('FA', 'Father'), + ('GM', 'Grandmother'), + ('GP', 'Grandfather'), + ('SM', 'Stepmother'), + ('SF', 'Stepfather'), + ('OT', 'Other'), + ] + students = models.ManyToManyField(Student, blank=True) + first_name = models.CharField(max_length=50, blank=True) + last_name = models.CharField(max_length=50, blank=True) + relation = models.CharField( + max_length=2, + choices=RELATION_CHOICES, + default='MO', + ) + phone_number = models.IntegerField(blank=True, null=True) + email_address = models.EmailField(blank=True, null=True) + notes = models.TextField(blank=True) + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + def get_absolute_url(self): + return reverse('parent-detail', kwargs={'pk': self.pk}) + + +class Thread(models.Model): + parent = models.ForeignKey(Parent, on_delete=models.CASCADE, blank=True, null=True) + student = models.ForeignKey(Student, on_delete=models.CASCADE, blank=True, null=True) + subject = models.CharField(max_length=250) + + def __str__(self): + return f"{self.parent} | {self.subject}" + + def get_absolute_url(self): + return reverse('thread-detail', kwargs={'pk': self.pk}) + + +class Message(models.Model): + class Meta: + ordering = ('-date',) + + thread = models.ForeignKey(Thread, on_delete=models.CASCADE) + date = models.DateField(blank=True, null=True) + time = models.TimeField(blank=True, null=True) + content = models.TextField() + + PHONE = 'PH' + EMAIL = 'EM' + NOTE = 'NO' + IN_PERSON = 'IP' + + MOC_CHOICES = [ + (PHONE, 'Phone'), + (EMAIL, 'Email'), + (NOTE, 'Note'), + (IN_PERSON, 'In person'), + ] + + method_of_contact = models.CharField( + max_length=2, + choices=MOC_CHOICES, + blank=True, + ) + + def __str__(self): + return f"{self.date} {self.thread.subject}" + + def get_absolute_url(self): + return reverse('message-detail', kwargs={'pk': self.thread.pk, 'message_pk': self.pk}) diff --git a/src/students/templates/students/message_confirm_delete.html b/src/students/templates/students/message_confirm_delete.html new file mode 100644 index 0000000..dc4be73 --- /dev/null +++ b/src/students/templates/students/message_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{message}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/message_create_form.html b/src/students/templates/students/message_create_form.html new file mode 100644 index 0000000..ebcc1ef --- /dev/null +++ b/src/students/templates/students/message_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{thread.subject}}

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/message_detail.html b/src/students/templates/students/message_detail.html new file mode 100644 index 0000000..78770bb --- /dev/null +++ b/src/students/templates/students/message_detail.html @@ -0,0 +1,8 @@ +
+ + {{message.date|date:"D, M j"}}
+ {{message.time|time:"TIME_FORMAT"}} via {{message.get_method_of_contact_display}} +
+ Edit +

{{message.content|linebreaksbr}}

+
\ No newline at end of file diff --git a/src/students/templates/students/message_form.html b/src/students/templates/students/message_form.html new file mode 100644 index 0000000..77e64ef --- /dev/null +++ b/src/students/templates/students/message_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/message_list.html b/src/students/templates/students/message_list.html new file mode 100644 index 0000000..61ab58c --- /dev/null +++ b/src/students/templates/students/message_list.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/parent_confirm_delete.html b/src/students/templates/students/parent_confirm_delete.html new file mode 100644 index 0000000..7f4631d --- /dev/null +++ b/src/students/templates/students/parent_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{parent}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/parent_create_form.html b/src/students/templates/students/parent_create_form.html new file mode 100644 index 0000000..fc26aa9 --- /dev/null +++ b/src/students/templates/students/parent_create_form.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Parent

+

For {{student}}

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/parent_detail.html b/src/students/templates/students/parent_detail.html new file mode 100644 index 0000000..48deb2a --- /dev/null +++ b/src/students/templates/students/parent_detail.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} +{% load students_filters %} + +{% block content %} +
+
+

{{parent.full_name}}

+ Update parent details +
+
+
+
Phone Number
+
{{parent.phone_number|phone_format}}
+
Email
+
{{parent.email_address}}
+ {% if parent.address %} +
Address
+
+
{{parent.address|linebreaksbr}}
+
+ {% endif %} + {% if parent.notes %} +
Notes
+
{{parent.notes|linebreaksbr}}
+ {% endif %} +
+
+
+

Communications

+ +
+
+

Parent to:

+
    + {% for student in parent.students.all %} +
  • + {{student.full_name}} + {% if student.sit %} + SIT: {{student.get_sit_display}} + {% endif %} + {% if student.iep_behavioral %} + IEP behavioral + {% endif %} + {% if student.iep_math %} + IEP math + {% endif %} + {% if student.iep_ela %} + IEP ELA + {% endif %} +
  • + {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/parent_form.html b/src/students/templates/students/parent_form.html new file mode 100644 index 0000000..26494a5 --- /dev/null +++ b/src/students/templates/students/parent_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/parent_list.html b/src/students/templates/students/parent_list.html new file mode 100644 index 0000000..c1e1d11 --- /dev/null +++ b/src/students/templates/students/parent_list.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/student_confirm_delete.html b/src/students/templates/students/student_confirm_delete.html new file mode 100644 index 0000000..e33bef1 --- /dev/null +++ b/src/students/templates/students/student_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{student}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/student_create_form.html b/src/students/templates/students/student_create_form.html new file mode 100644 index 0000000..58e6631 --- /dev/null +++ b/src/students/templates/students/student_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Student

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/student_detail.html b/src/students/templates/students/student_detail.html new file mode 100644 index 0000000..f872cb2 --- /dev/null +++ b/src/students/templates/students/student_detail.html @@ -0,0 +1,162 @@ +{% extends "base.html" %} +{% load students_filters %} + +{% block content %} +
+
+
+
+

{{student.student_id}} — {{student}}

+

+
+ Update student details +
+
+ {% if student.glasses %} + Glasses + {% endif %} + {% if student.sit %} + SIT: {{student.get_sit_display}} + {% endif %} + {% if student.iep_behavioral %} + IEP behavioral + {% endif %} + {% if student.iep_math %} + IEP math + {% endif %} + {% if student.iep_ela %} + IEP ELA + {% endif %} +
+
+ {% if student.allergies %} +
Allergies
+
{{student.allergies}}
+ {% endif %} +
Birthday
+
{{student.dob}}
+
Age
+
{{student.age}}
+ {% if student.address %} +
Address
+
+
{{student.address|linebreaksbr}}
+
+ {% endif %} +
+
+
+

Grades

+ + + {% for subject in subject_list %} + + + + + {% empty %} + + + + {% endfor %} + +
{{subject}}{{subject.grade|grade_as_percentage:subject.grade_total}}%
No grades yet.
+
+
+

Parents

+

+ + New parent +

+
+ {% for parent in student.parent_set.all %} +
+ {{parent.get_relation_display}} +

{{parent.full_name}}

+
+
Phone Number
+
{{parent.phone_number|phone_format}}
+
Email
+
{{parent.email_address}}
+ {% if parent.address %} +
Address
+
+
{{parent.address|linebreaksbr}}
+
+ {% endif %} + {% if parent.notes %} +
Notes
+
{{parent.notes|linebreaksbr}}
+ {% endif %} +
+
+ {% empty %} +

No parents yet.

+ {% endfor %} +
+
+ +
+

Gradebook

+ +
+ {% regroup score_list by component.subject as score_list %} + {% for subject in score_list %} +

{{subject.grouper}}

+ + + + + + + + + + + + + {% for score in subject.list %} + + + + + + + + + + {% endfor %} + +
Due DateComponentCategoryScoreTotalPercentage
{{score.component.due_date}}{{score.component}}{{score.component.get_category_display}}{{score.value}}{{score.component.grade_total}}{{score.grade_as_percentage}}%Change score
+ {% empty %} +

No components graded yet.

+ {% endfor %} +
+
+ +
+

Attendance

+
+ {% regroup entry_list by get_status_display as rentry_list %} + {% for status in rentry_list %} +

{{ status.grouper }}

+ + + + + + + + + {% for entry in status.list %} + + + + + {% endfor %} + +
DateStatus
{{entry.day.date}}Update
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/src/students/templates/students/student_form.html b/src/students/templates/students/student_form.html new file mode 100644 index 0000000..be0bc2a --- /dev/null +++ b/src/students/templates/students/student_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/student_list.html b/src/students/templates/students/student_list.html new file mode 100644 index 0000000..0e45076 --- /dev/null +++ b/src/students/templates/students/student_list.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Students

+

+ See all parents → +

+

+ + New student +

+
+
+
    + {% for student in student_list %} +
  • + {{student.student_id}} — {{student.full_name}} + + {% if student.glasses %} + Glasses + {% endif %} + {% if student.sit %} + SIT: {{student.get_sit_display}} + {% endif %} + {% if student.iep_behavioral %} + IEP behavioral + {% endif %} + {% if student.iep_math %} + IEP math + {% endif %} + {% if student.iep_ela %} + IEP ELA + {% endif %} + {% if student.allergies %} +
    Allergies: {{student.allergies}} + {% endif %} + {% if student.parent__count > 0 %} +
    Parents: + {% for parent in student.parent_set.all %} + {{parent.full_name}}{% if not forloop.last %},{% endif %} + {% endfor %} + {% endif %} +
  • + {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/thread_confirm_delete.html b/src/students/templates/students/thread_confirm_delete.html new file mode 100644 index 0000000..ad6a637 --- /dev/null +++ b/src/students/templates/students/thread_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{thread}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/thread_create_form.html b/src/students/templates/students/thread_create_form.html new file mode 100644 index 0000000..8ec37b3 --- /dev/null +++ b/src/students/templates/students/thread_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Thread

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/thread_detail.html b/src/students/templates/students/thread_detail.html new file mode 100644 index 0000000..5801b45 --- /dev/null +++ b/src/students/templates/students/thread_detail.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/thread_form.html b/src/students/templates/students/thread_form.html new file mode 100644 index 0000000..62e2d04 --- /dev/null +++ b/src/students/templates/students/thread_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/src/students/templates/students/thread_list.html b/src/students/templates/students/thread_list.html new file mode 100644 index 0000000..f23f27f --- /dev/null +++ b/src/students/templates/students/thread_list.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +
+

Communications

+
+

+ + New thread +

+
    + {% for thread in thread_list %} +
  • + {{thread.subject}} + {{thread.message__count}}
    + {% if thread.parent %} + Parent: {{thread.parent}}
    + {% endif %} + {% if thread.student %} + Student: {{thread.student}} + {% endif %} +
  • + {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/students/templatetags/__init__.py b/src/students/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/students/templatetags/students_filters.py b/src/students/templatetags/students_filters.py new file mode 100644 index 0000000..d0e7c3f --- /dev/null +++ b/src/students/templatetags/students_filters.py @@ -0,0 +1,35 @@ +from django import template + +register = template.Library() + +@register.filter() +def timedelta_format(value, arg=2): + """Returns timedelta as float rounded to arg, with default of 2""" + if value.days: + return round((value.days*24) + value.seconds/3600, arg) + elif value.seconds: + return round(value.seconds/3600, arg) + return 0 + + +@register.filter() +def phone_format(value): + value = str(value) + if value: + if len(value) == 10: + area_code = value[:3] + prefix = value[3:6] + line = value[6:10] + return f"+1-{area_code}-{prefix}-{line}" + elif len(value) == 11: + country = value[:1] + area_code = value[1:4] + prefix = value[4:7] + line = value[7:11] + return f"+{country}-{area_code}-{prefix}-{line}" + else: + return "No phone number." + +@register.filter() +def grade_as_percentage(numerator, denominator): + return round(numerator / denominator * 100, 2) \ No newline at end of file diff --git a/src/students/tests.py b/src/students/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/students/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/students/urls.py b/src/students/urls.py new file mode 100644 index 0000000..b0dc9c8 --- /dev/null +++ b/src/students/urls.py @@ -0,0 +1,43 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + path('', views.StudentListView.as_view(), name='student-list'), + path('new/', views.StudentCreateView.as_view(), name='student-create'), + path('/', include([ + path('', views.StudentDetailView.as_view(), name='student-detail'), + path('update/', views.StudentUpdateView.as_view(), name='student-update'), + path('delete/', views.StudentDeleteView.as_view(), name='student-delete'), + + + ])), + + path('parents/', views.ParentListView.as_view(), name='parent-list'), + path('parents/new/', views.ParentCreateView.as_view(), name='parent-create'), + path('parents//', include([ + path('', views.ParentDetailView.as_view(), name='parent-detail'), + path('update/', views.ParentUpdateView.as_view(), name='parent-update'), + path('delete/', views.ParentDeleteView.as_view(), name='parent-delete'), + ])), + + + path('threads/', views.ThreadListView.as_view(), name='thread-list'), + path('threads/new/', views.ThreadCreateView.as_view(), name='thread-create'), + path('threads//', include([ + path('', views.ThreadDetailView.as_view(), name='thread-detail'), + path('update/', views.ThreadUpdateView.as_view(), name='thread-update'), + path('delete/', views.ThreadDeleteView.as_view(), name='thread-delete'), + + + path('messages/', views.MessageListView.as_view(), name='message-list'), + path('messages/new/', views.MessageCreateView.as_view(), name='message-create'), + path('messages//', include([ + path('', views.MessageDetailView.as_view(), name='message-detail'), + path('update/', views.MessageUpdateView.as_view(), name='message-update'), + path('delete/', views.MessageDeleteView.as_view(), name='message-delete'), + ])), + ])), + + + +] diff --git a/src/students/views.py b/src/students/views.py new file mode 100644 index 0000000..99914d3 --- /dev/null +++ b/src/students/views.py @@ -0,0 +1,206 @@ +from django.shortcuts import render +from django.urls import reverse, reverse_lazy +from django.utils import timezone +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from django.contrib.auth.mixins import LoginRequiredMixin + +from django.db.models import Prefetch, Subquery, Count, Sum, F, Q, Value +from django.db.models.functions import Length, Upper + +from .models import Student, Parent, Thread, Message +from .forms import StudentForm, ParentForm, ThreadForm, MessageForm + +from gradebook.models import ( + Tag, + Subject, + Component, + Score, +) + +from attendance.models import Entry + + +class StudentListView(LoginRequiredMixin, ListView): + model = Student + + def get_queryset(self): + object_list = Student.objects.annotate( + Count('parent') + ).prefetch_related('parent_set').order_by('student_id') + return object_list + +class StudentCreateView(LoginRequiredMixin, CreateView): + model = Student + form_class = StudentForm + template_name_suffix = '_create_form' + +class StudentDetailView(LoginRequiredMixin, DetailView): + model = Student + + def get_queryset(self): + object_list = Student.objects.all().prefetch_related('parent_set') + return object_list + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['score_list'] = Score.objects.select_related( + 'student' + ).prefetch_related( + 'component' + ).select_related( + 'component__subject' + ).filter( + student=self.object + ).order_by( + 'component__subject', + '-component__due_date' + ) + + context['subject_list'] = Subject.objects.filter( + component__score__student=self.object + ).annotate( + grade=Sum(F('component__score__value')), + grade_total=Sum('component__grade_total') + ) + + context['entry_list'] = Entry.objects.select_related( + 'day' + ).filter( + student=self.object + ).order_by('status', '-day__date').select_related('student') + + return context + + +class StudentUpdateView(LoginRequiredMixin, UpdateView): + model = Student + form_class = StudentForm + + def get_success_url(self): + pk = self.kwargs["pk"] + return reverse('student-detail', kwargs={'pk': pk}) + +class StudentDeleteView(LoginRequiredMixin, DeleteView): + model = Student + success_url = reverse_lazy('student-list') + + + +class ParentListView(LoginRequiredMixin, ListView): + model = Parent + +class ParentCreateView(LoginRequiredMixin, CreateView): + model = Parent + form_class = ParentForm + template_name_suffix = '_create_form' + +class ParentDetailView(LoginRequiredMixin, DetailView): + model = Parent + +class ParentUpdateView(LoginRequiredMixin, UpdateView): + model = Parent + form_class = ParentForm + +class ParentDeleteView(LoginRequiredMixin, DeleteView): + model = Parent + success_url = reverse_lazy('parent-list') + + + +class ThreadListView(LoginRequiredMixin, ListView): + model = Thread + + def get_queryset(self): + object_list = Thread.objects.annotate( + Count('message') + ) + return object_list + +class ThreadCreateView(LoginRequiredMixin, CreateView): + model = Thread + form_class = ThreadForm + template_name_suffix = '_create_form' + +class ThreadDetailView(LoginRequiredMixin, DetailView): + model = Thread + + def get_object(self): + queryset = Thread.objects.filter( + pk=self.kwargs.get(self.pk_url_kwarg) + ).prefetch_related('message_set').select_related('parent', 'student') + obj = queryset.get() + return obj + +class ThreadUpdateView(LoginRequiredMixin, UpdateView): + model = Thread + form_class = ThreadForm + +class ThreadDeleteView(LoginRequiredMixin, DeleteView): + model = Thread + success_url = reverse_lazy('thread-list') + + +# Messags +class MessageListView(LoginRequiredMixin, ListView): + model = Message + pk_url_kwarg = 'message_pk' + +class MessageCreateView(LoginRequiredMixin, CreateView): + model = Message + pk_url_kwarg = 'message_pk' + template_name_suffix = '_create_form' + form_class = MessageForm + + def get_initial(self): + today = timezone.localtime(timezone.now()).date() + initial = { + 'date': today, + } + return initial + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['thread'] = Thread.objects.get(pk=self.kwargs['pk']) + return context + + def form_valid(self, form): + form.instance.thread = Thread.objects.get(pk=self.kwargs['pk']) + return super().form_valid(form) + + def get_success_url(self): + return reverse('thread-detail', kwargs={'pk': self.kwargs['pk']}) + +class MessageDetailView(LoginRequiredMixin, DetailView): + model = Message + pk_url_kwarg = 'message_pk' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['thread'] = Thread.objects.get(pk=self.kwargs['pk']) + return context + +class MessageUpdateView(LoginRequiredMixin, UpdateView): + model = Message + pk_url_kwarg = 'message_pk' + form_class = MessageForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['thread'] = Thread.objects.get(pk=self.kwargs['pk']) + return context + + def get_success_url(self): + return reverse('thread-detail', kwargs={'pk': self.kwargs['pk']}) + + + +class MessageDeleteView(LoginRequiredMixin, DeleteView): + model = Message + pk_url_kwarg = 'message_pk' + + def get_success_url(self): + return reverse('thread-detail', kwargs={'pk': self.kwargs['pk']}) diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 0000000..b967abf --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,64 @@ +{% load static %} +{% load compress %} + + + + + + + {% block head_title %}{% endblock %} Indici + + + + + {% compress css %} + + + {% endcompress %} + + {% block head %} + {% endblock %} + + + +
+ {% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+ {% endif %} + + {% block breadcrumbs %} + {% endblock breadcrumbs %} + + {% block content %} + {% endblock content %} +
+ + diff --git a/src/templates/flatpages/base.html b/src/templates/flatpages/base.html new file mode 100644 index 0000000..47523e9 --- /dev/null +++ b/src/templates/flatpages/base.html @@ -0,0 +1,42 @@ +{% load static %} + + + + + + + Indici + + + + + + + +
+ +
+
+{{flatpage.content}} +
+ + \ No newline at end of file diff --git a/src/templates/registration/logged_out.html b/src/templates/registration/logged_out.html new file mode 100755 index 0000000..0b464dc --- /dev/null +++ b/src/templates/registration/logged_out.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

You have been logged out. Log in

+
+
+{% endblock %} diff --git a/src/templates/registration/login.html b/src/templates/registration/login.html new file mode 100755 index 0000000..b7d4b80 --- /dev/null +++ b/src/templates/registration/login.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Log in

+ {% if form.errors %} +

Your username and password didn't match. Please try again.

+ {% endif %} + +
+ {% csrf_token %} +

+ {{ form.username.label_tag }} + {{ form.username }} +

+ +

+ {{ form.password.label_tag }} + {{ form.password }} +
+ + Forgot your password? + Reset your password here. + +

+ +

+ + +

+
+
+{% endblock %} diff --git a/src/templates/registration/password_change_done.html b/src/templates/registration/password_change_done.html new file mode 100755 index 0000000..fb611b7 --- /dev/null +++ b/src/templates/registration/password_change_done.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

+ Password has been changed. + Log in +

+
+
+{% endblock %} diff --git a/src/templates/registration/password_change_form.html b/src/templates/registration/password_change_form.html new file mode 100755 index 0000000..cad7d69 --- /dev/null +++ b/src/templates/registration/password_change_form.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Change password

+
+ {% csrf_token %} + {{ form.as_p }} + + or + Cancel +
+
+{% endblock %} diff --git a/src/templates/registration/password_reset_complete.html b/src/templates/registration/password_reset_complete.html new file mode 100755 index 0000000..ad0c905 --- /dev/null +++ b/src/templates/registration/password_reset_complete.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Password was reset successfully.

+
+{% endblock %} diff --git a/src/templates/registration/password_reset_confirm.html b/src/templates/registration/password_reset_confirm.html new file mode 100755 index 0000000..1aaf4a8 --- /dev/null +++ b/src/templates/registration/password_reset_confirm.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} + +{% if validlink %} +
+

Reset password

+

Enter a new password below.

+
+ {% csrf_token %} + {{ form.as_p }} + + +
+
+{% else %} + +
+

Password reset failed

+
+ +{% endif %} + +{% endblock %} diff --git a/src/templates/registration/password_reset_done.html b/src/templates/registration/password_reset_done.html new file mode 100755 index 0000000..8f2b394 --- /dev/null +++ b/src/templates/registration/password_reset_done.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +
+

An email with password reset instructions has been sent.

+
+{% endblock %} diff --git a/src/templates/registration/password_reset_email.html b/src/templates/registration/password_reset_email.html new file mode 100755 index 0000000..8c37094 --- /dev/null +++ b/src/templates/registration/password_reset_email.html @@ -0,0 +1,10 @@ +{% load i18n %}{% autoescape off %} +{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %} + +{% translate "Please go to the following page and choose a new password:" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} +{% endblock %} +{% translate 'Your username, in case you’ve forgotten:' %} {{ user.get_username }} + +{% endautoescape %} diff --git a/src/templates/registration/password_reset_form.html b/src/templates/registration/password_reset_form.html new file mode 100755 index 0000000..1a21366 --- /dev/null +++ b/src/templates/registration/password_reset_form.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} {% block content %} +
+

Reset your password

+

Enter your email address below and we'll send you instructions on how to reset your password.

+
+ {% csrf_token %} + {{ form.as_p }} + +

+ +

+
+
+{% endblock %} diff --git a/templates/_confirm_delete.html b/templates/_confirm_delete.html new file mode 100644 index 0000000..6c7601c --- /dev/null +++ b/templates/_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +

Delete object_name

+
+ {% csrf_token %} +

Are you sure you want to delete "{{ object }}"?

+ {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/templates/_create_form.html b/templates/_create_form.html new file mode 100644 index 0000000..b915b6b --- /dev/null +++ b/templates/_create_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

+ New object_name

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/templates/_detail.html b/templates/_detail.html new file mode 100644 index 0000000..c2174df --- /dev/null +++ b/templates/_detail.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block content %} +

object_name

+

+ Edit + Delete +

+

{{ object }}

+{% endblock %} diff --git a/templates/_form.html b/templates/_form.html new file mode 100644 index 0000000..7afeed3 --- /dev/null +++ b/templates/_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

Update object_name

+
+ {% csrf_token %} + {{ form.as_p }} +

+ or cancel +

+
+{% endblock %} diff --git a/templates/_list.html b/templates/_list.html new file mode 100644 index 0000000..8320a50 --- /dev/null +++ b/templates/_list.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +

object_names

+ + + {% for object in object_list %} + +
{{ object }}
+ + {% empty %} + No objects yet. + {% endfor %} + +
+{% endblock %}