Initial commit
This commit is contained in:
commit
cd502df19a
130
.gitignore
vendored
Normal file
130
.gitignore
vendored
Normal file
@ -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/
|
||||||
16
Pipfile
Normal file
16
Pipfile
Normal file
@ -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"
|
||||||
135
Pipfile.lock
generated
Normal file
135
Pipfile.lock
generated
Normal file
@ -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": {}
|
||||||
|
}
|
||||||
63
generate_templates.py
Normal file
63
generate_templates.py
Normal file
@ -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()
|
||||||
25
readme.md
Normal file
25
readme.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Indici
|
||||||
|
|
||||||
|
## How To Start
|
||||||
|
|
||||||
|
|
||||||
|
### 1. Activate Virtualenv
|
||||||
|
|
||||||
|
`windows`
|
||||||
|
```cmd
|
||||||
|
<YOUR WORKING DIRECTORY>/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)
|
||||||
69
requirements.txt
Normal file
69
requirements.txt
Normal file
@ -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
|
||||||
17
setup.yaml
Normal file
17
setup.yaml
Normal file
@ -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
|
||||||
0
src/accounts/__init__.py
Normal file
0
src/accounts/__init__.py
Normal file
5
src/accounts/admin.py
Normal file
5
src/accounts/admin.py
Normal file
@ -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)
|
||||||
6
src/accounts/apps.py
Normal file
6
src/accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'accounts'
|
||||||
13
src/accounts/forms.py
Normal file
13
src/accounts/forms.py
Normal file
@ -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',
|
||||||
|
]
|
||||||
45
src/accounts/migrations/0001_initial.py
Normal file
45
src/accounts/migrations/0001_initial.py
Normal file
@ -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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/accounts/migrations/__init__.py
Normal file
0
src/accounts/migrations/__init__.py
Normal file
27
src/accounts/models.py
Normal file
27
src/accounts/models.py
Normal file
@ -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
|
||||||
|
)
|
||||||
14
src/accounts/templates/accounts/account_create.html
Executable file
14
src/accounts/templates/accounts/account_create.html
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="panel">
|
||||||
|
<h1>Sign up</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Create account">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
72
src/accounts/templates/accounts/account_detail.html
Executable file
72
src/accounts/templates/accounts/account_detail.html
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<header>
|
||||||
|
<h1 class="greeting"><em>Welcome {{user.first_name}} {{user.last_name }}</em>
|
||||||
|
<br>
|
||||||
|
Here's what's going on today
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Birthdays</h3>
|
||||||
|
<ul>
|
||||||
|
{% for student in birthdays %}
|
||||||
|
<li><strong><a href="{% url 'student-detail' student.pk %}">{{student}}</a></strong> is turning {{student.age|add:1}} on {{student.dob|date:"M j"}}</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>No Birthdays this next week.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Today's Assignments</h3>
|
||||||
|
<ul>
|
||||||
|
{% for component in components %}
|
||||||
|
<li>
|
||||||
|
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>Nothing for today.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Today's Attendance</h3>
|
||||||
|
{% for day in attendance %}
|
||||||
|
<p><strong><a href="{% url 'day-update' day.pk %}">{{day.date}}</a></strong></p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td colspan="2">Status</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in day.entry_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{entry.student}}</td>
|
||||||
|
<td>{{entry.get_status_display}}</td>
|
||||||
|
<td><a href="{% url 'entry-update' entry.pk %}">Update</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% empty %}
|
||||||
|
<p class="greeting">No attendance taken yet: <a href="{% url 'day-create' %}" class="action-button">Take attendance</a></p>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Assignments to be graded</h3>
|
||||||
|
<ul>
|
||||||
|
{% for component in ungraded_components %}
|
||||||
|
<li>
|
||||||
|
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>Everything is graded to far.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
18
src/accounts/templates/accounts/account_form.html
Executable file
18
src/accounts/templates/accounts/account_form.html
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Update Profile</h1>
|
||||||
|
<p><a href="{% url 'password_change' %}">Change password</a></p>
|
||||||
|
<form method="post" action="{% url 'account-update' user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" class="action-button"> or
|
||||||
|
<a href="{% url 'account-detail' user.id %}">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
23
src/accounts/templates/accounts/account_list.html
Executable file
23
src/accounts/templates/accounts/account_list.html
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="panel">
|
||||||
|
<h1>Users</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in user_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td><a href="{% url 'account-detail' user.id %}">{{user.first_name}} {{user.last_name}}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td>No users yet.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
72
src/accounts/templates/accounts/profile.html
Normal file
72
src/accounts/templates/accounts/profile.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<header>
|
||||||
|
<h1 class="greeting"><em>Welcome {{profile.user.first_name}} {{profile.user.last_name }}</em>
|
||||||
|
<br>
|
||||||
|
Here's what's going on today
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Birthdays</h3>
|
||||||
|
<ul>
|
||||||
|
{% for student in birthdays %}
|
||||||
|
<li><strong><a href="{% url 'student-detail' student.pk %}">{{student}}</a></strong> is turning {{student.age|add:1}} on {{student.dob|date:"M j"}}</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>No Birthdays this next week.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Today's Assignments</h3>
|
||||||
|
<ul>
|
||||||
|
{% for component in components %}
|
||||||
|
<li>
|
||||||
|
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>Nothing for today.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Today's Attendance</h3>
|
||||||
|
{% for day in attendance %}
|
||||||
|
<p><strong><a href="{% url 'day-update' day.pk %}">{{day.date}}</a></strong></p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td colspan="2">Status</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in day.entry_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{entry.student}}</td>
|
||||||
|
<td>{{entry.get_status_display}}</td>
|
||||||
|
<td><a href="{% url 'entry-update' entry.pk %}">Update</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% empty %}
|
||||||
|
<p class="greeting">No attendance taken yet: <a href="{% url 'day-create' %}" class="action-button">Take attendance</a></p>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3 class="domain__heading">Assignments to be graded</h3>
|
||||||
|
<ul>
|
||||||
|
{% for component in ungraded_components %}
|
||||||
|
<li>
|
||||||
|
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>Everything is graded to far.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
10
src/accounts/templates/accounts/profile_form.html
Executable file
10
src/accounts/templates/accounts/profile_form.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<section>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<form action="{% url 'profile-update' user.profile.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Save changes" class="action-button">
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
3
src/accounts/tests.py
Normal file
3
src/accounts/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
11
src/accounts/urls.py
Normal file
11
src/accounts/urls.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.AccountListView.as_view(), name='account-list'),
|
||||||
|
path('<int:pk>/', 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'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
75
src/accounts/views.py
Normal file
75
src/accounts/views.py
Normal file
@ -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')
|
||||||
0
src/attendance/__init__.py
Normal file
0
src/attendance/__init__.py
Normal file
6
src/attendance/admin.py
Normal file
6
src/attendance/admin.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Day, Entry
|
||||||
|
|
||||||
|
admin.site.register(Day)
|
||||||
|
admin.site.register(Entry)
|
||||||
6
src/attendance/apps.py
Normal file
6
src/attendance/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'attendance'
|
||||||
24
src/attendance/forms.py
Normal file
24
src/attendance/forms.py
Normal file
@ -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()
|
||||||
|
}
|
||||||
39
src/attendance/migrations/0001_initial.py
Normal file
39
src/attendance/migrations/0001_initial.py
Normal file
@ -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',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/attendance/migrations/__init__.py
Normal file
0
src/attendance/migrations/__init__.py
Normal file
33
src/attendance/models.py
Normal file
33
src/attendance/models.py
Normal file
@ -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}"
|
||||||
13
src/attendance/templates/attendance/day_confirm_delete.html
Normal file
13
src/attendance/templates/attendance/day_confirm_delete.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{day}}</h1>
|
||||||
|
<form method="post" action="{% url 'day-delete' day.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{day}}"> or <a href="{% url 'day-detail' day.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
34
src/attendance/templates/attendance/day_create_form.html
Normal file
34
src/attendance/templates/attendance/day_create_form.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<form action="{% url 'day-create' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<header>
|
||||||
|
<h1>Take Attendance</h1>
|
||||||
|
{{form.as_p}}
|
||||||
|
</header>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td>Present</td>
|
||||||
|
<td>Tardy</td>
|
||||||
|
<td>Absent</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for student in student_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{student.full_name}}</td>
|
||||||
|
<td><input type="radio" name="students_{{student.pk}}" value="P" checked></td>
|
||||||
|
<td><input type="radio" name="students_{{student.pk}}" value="T"></td>
|
||||||
|
<td><input type="radio" name="students_{{student.pk}}" value="A"></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<input class="action-button" type="submit" value="Save attendance"> or <a href="{% url 'day-list' %}">cancel</a>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
34
src/attendance/templates/attendance/day_form.html
Normal file
34
src/attendance/templates/attendance/day_form.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<form action="{% url 'day-update' day.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<header>
|
||||||
|
<h1>Take Attendance</h1>
|
||||||
|
{{form.as_p}}
|
||||||
|
</header>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td>Present</td>
|
||||||
|
<td>Tardy</td>
|
||||||
|
<td>Absent</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in day.entry_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{entry.student.full_name}}</td>
|
||||||
|
<td><input type="radio" name="students_{{entry.student.pk}}" value="P" {% if entry.status == "P" %}checked{% endif %}></td>
|
||||||
|
<td><input type="radio" name="students_{{entry.student.pk}}" value="T" {% if entry.status == "T" %}checked{% endif %}></td>
|
||||||
|
<td><input type="radio" name="students_{{entry.student.pk}}" value="A" {% if entry.status == "A" %}checked{% endif %}></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'day-list' %}">cancel</a>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
51
src/attendance/templates/attendance/day_list.html
Normal file
51
src/attendance/templates/attendance/day_list.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Attendance</h1>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'day-create' %}" class="action-button">Take attendance</a>
|
||||||
|
</p>
|
||||||
|
<section>
|
||||||
|
{% for day in day_list %}
|
||||||
|
<h4><a href="{% url 'day-update' day.pk %}">{{day.date}}</a></h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td colspan="2">Status</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in day.entry_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{entry.student}}</td>
|
||||||
|
<td>{{entry.get_status_display}}</td>
|
||||||
|
<td><a href="{% url 'entry-update' entry.pk %}">Update</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% empty %}
|
||||||
|
<p>No attendance taken yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<section class="pagination">
|
||||||
|
<span class="step-links">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a class="minor-action-button" href="?page=1">« first</a>
|
||||||
|
<a class="minor-action-button" href="?page={{ page_obj.previous_page_number }}">‹ previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="current">
|
||||||
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a class="minor-action-button" href="?page={{ page_obj.next_page_number }}">next ›</a>
|
||||||
|
<a class="minor-action-button" href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{entry}}</h1>
|
||||||
|
<form method="post" action="{% url 'entry-delete' entry.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{entry}}"> or <a href="{% url 'day-detail' entry.day.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
20
src/attendance/templates/attendance/entry_form.html
Normal file
20
src/attendance/templates/attendance/entry_form.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="generic__header">
|
||||||
|
<h1>Update Entry</h1>
|
||||||
|
<a class="action-button action-delete" href="{% url 'entry-delete' entry.pk %}">Delete {{entry}}</a>
|
||||||
|
</div>
|
||||||
|
<p>For <strong>{{student}}</strong></p>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'entry-update' entry.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'student-detail' entry.student.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
3
src/attendance/tests.py
Normal file
3
src/attendance/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
20
src/attendance/urls.py
Normal file
20
src/attendance/urls.py
Normal file
@ -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/<int:pk>/', 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/<int:pk>/', 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'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
98
src/attendance/views.py
Normal file
98
src/attendance/views.py
Normal file
@ -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')
|
||||||
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
3
src/core/admin.py
Normal file
3
src/core/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
src/core/apps.py
Normal file
6
src/core/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'core'
|
||||||
37
src/core/forms.py
Normal file
37
src/core/forms.py
Normal file
@ -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',
|
||||||
|
}
|
||||||
114
src/core/migrations/0001_initial.py
Normal file
114
src/core/migrations/0001_initial.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/core/migrations/0002_alter_schoolyear_start_date.py
Normal file
18
src/core/migrations/0002_alter_schoolyear_start_date.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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,
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/core/migrations/0004_alter_schoolyear_year.py
Normal file
18
src/core/migrations/0004_alter_schoolyear_year.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
104
src/core/migrations/0005_attendanceentry_created_at_and_more.py
Normal file
104
src/core/migrations/0005_attendanceentry_created_at_and_more.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
src/core/migrations/0006_score.py
Normal file
26
src/core/migrations/0006_score.py
Normal file
@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/core/migrations/__init__.py
Normal file
0
src/core/migrations/__init__.py
Normal file
259
src/core/models.py
Normal file
259
src/core/models.py
Normal file
@ -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']
|
||||||
17
src/core/templates/core/partials/pagination.html
Normal file
17
src/core/templates/core/partials/pagination.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<section class="pagination">
|
||||||
|
<span class="step-links">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a href="?page=1">« first</a>
|
||||||
|
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="current">
|
||||||
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a href="?page={{ page_obj.next_page_number }}">next</a>
|
||||||
|
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
13
src/core/templates/core/schoolyear_confirm_delete.html
Normal file
13
src/core/templates/core/schoolyear_confirm_delete.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Delete Schoolyear</h1>
|
||||||
|
<form method="POST" action="{% url 'core:schoolyear-delete' schoolyear.year %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Are you sure you want to delete "{{ schoolyear }}"?</p>
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Delete"> or <a href="{% url 'core:schoolyear-detail' schoolyear.year %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
12
src/core/templates/core/schoolyear_create_form.html
Normal file
12
src/core/templates/core/schoolyear_create_form.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>+ New Schoolyear</h1>
|
||||||
|
<form method="POST" action="{% url 'core:schoolyear-create' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Create"> or <a href="{% url 'core:schoolyear-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
34
src/core/templates/core/schoolyear_detail.html
Normal file
34
src/core/templates/core/schoolyear_detail.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block head_title %}Project {{ schoolyear.name }} | {% endblock head_title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="detail">
|
||||||
|
<header class="detail__header">
|
||||||
|
<div>
|
||||||
|
<h1>{{ schoolyear }}</h1>
|
||||||
|
<p><small>Created <time datetime="{{ schoolyear.created_at|date:'Y-m-d' }}">{{ schoolyear.created_at }}</time></small></p>
|
||||||
|
</div>
|
||||||
|
{% if perms.core.can_change_schoolyear %}
|
||||||
|
<a href="{% url 'core:schoolyear-update' schoolyear.year %}" class="action-button">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
<div class="tools">
|
||||||
|
<a href="{% url 'core:student-list' schoolyear.year %}" class="tool">
|
||||||
|
<h3>Students</h3>
|
||||||
|
<p>Tool Description</p>
|
||||||
|
<span>→</span>
|
||||||
|
</a>
|
||||||
|
<a href="" class="tool">
|
||||||
|
<h3>Attendence</h3>
|
||||||
|
<p>Tool Description</p>
|
||||||
|
<span>→</span>
|
||||||
|
</a>
|
||||||
|
<a href="" class="tool">
|
||||||
|
<h3>Subjects</h3>
|
||||||
|
<p>Tool Description</p>
|
||||||
|
<span>→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
12
src/core/templates/core/schoolyear_form.html
Normal file
12
src/core/templates/core/schoolyear_form.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Update Schoolyear</h1>
|
||||||
|
<form method="POST" action="{% url 'core:schoolyear-update' schoolyear.year %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Save changes"> or <a href="{% url 'core:schoolyear-detail' schoolyear.year %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
46
src/core/templates/core/schoolyear_list.html
Normal file
46
src/core/templates/core/schoolyear_list.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block head_title %}School Years | {% endblock head_title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="list">
|
||||||
|
<header class="list__header">
|
||||||
|
<div class="list__title">
|
||||||
|
<h1>School Years</h1>
|
||||||
|
{% if perms.core.can_add_schoolyear %}
|
||||||
|
<a href="{% url 'core:schoolyear-create' %}" class="action-button">+ New Year</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<table class="schoolyears list__table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Year</th>
|
||||||
|
<th>Students</th>
|
||||||
|
<th>Components</th>
|
||||||
|
<th>Last Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="schoolyears__list">
|
||||||
|
{% for schoolyear in schoolyear_list %}
|
||||||
|
<tr class="schoolyear has-link" onclick="document.location='{% url 'core:schoolyear-detail' schoolyear.year %}'">
|
||||||
|
<td>{{ schoolyear.created_at|date:'m/d/Y' }}</td>
|
||||||
|
<td><h5>{{ schoolyear.year }}</h5></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">No schoolyears yet.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% include 'core/partials/pagination.html' %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
13
src/core/templates/core/student_confirm_delete.html
Normal file
13
src/core/templates/core/student_confirm_delete.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Delete Student</h1>
|
||||||
|
<form method="POST" action="{% url 'core:student-delete' student.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Are you sure you want to delete "{{ student }}"?</p>
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Delete"> or <a href="{% url 'core:student-detail' student.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
24
src/core/templates/core/student_create_form.html
Normal file
24
src/core/templates/core/student_create_form.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<menu>
|
||||||
|
<li><strong><a href="{% url 'core:schoolyear-detail' school_year.year %}">{{ school_year.year }}</a></strong></li>
|
||||||
|
<span>›</span>
|
||||||
|
<li><a href="{% url 'core:student-list' school_year.year %}">Students</a></li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="form">
|
||||||
|
<h1>+ New student</h1>
|
||||||
|
<form method="POST" action="{% url 'core:student-create' school_year.year %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Create"> or <a href="{% url 'core:student-list' school_year.year %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
131
src/core/templates/core/student_detail.html
Normal file
131
src/core/templates/core/student_detail.html
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block head_title %}Student {{ student.student_id }} | {% endblock head_title %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<menu>
|
||||||
|
<li><strong><a href="{% url 'core:schoolyear-detail' student.school_year.year %}">{{ student.school_year.year }}</a></strong></li>
|
||||||
|
<span>›</span>
|
||||||
|
<li><a href="{% url 'core:student-list' student.school_year.year %}">Students</a></li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="detail">
|
||||||
|
<header class="detail__header">
|
||||||
|
<div>
|
||||||
|
<h1>{{ student.student_id }}   /   {{ student.full_name }}</h1>
|
||||||
|
<p><small>Added: <time datetime="{{ student.created_at|date:'Y-m-d' }}">{{ student.created_at|date:'M d Y' }}</time><br>
|
||||||
|
Last updated: <time datetime="{{ student.updated_at|date:'Y-m-d' }}">{{ student.updated_at|date:'M d Y' }}</time></small></p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'core:student-update' student.school_year.year student.pk %}" class="action-button">Edit</a>
|
||||||
|
</header>
|
||||||
|
<section class="student__details">
|
||||||
|
<dl>
|
||||||
|
{% if student.allergies %}
|
||||||
|
<dt>Allergies</dt>
|
||||||
|
<dd>{{ student.allergies }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
<dt>Birthday</dt>
|
||||||
|
<dd>{{ student.dob }}</dd>
|
||||||
|
<dt>Age</dt>
|
||||||
|
<dd>{{ student.age }}</dd>
|
||||||
|
{% if student.address %}
|
||||||
|
<dt>Address</dt>
|
||||||
|
<dd>
|
||||||
|
<address>{{ student.address|linebreaksbr }}</address>
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section class="grades">
|
||||||
|
<h2>Grades</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Subject</th>
|
||||||
|
<th>Grade</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for subject in subject_list %}
|
||||||
|
<tr>
|
||||||
|
<td class="grade"><em>{{subject}}</em></td>
|
||||||
|
<td class="grade">{{subject.grade|grade_as_percentage:subject.grade_total}}%</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">No grades yet. To add a grade you will need to enter a score for this student on a component.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Components</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% regroup score_list by component.subject as score_list %}
|
||||||
|
{% for subject in score_list %}
|
||||||
|
<h4>{{subject.grouper}}</h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Due Date</td>
|
||||||
|
<td>Component</td>
|
||||||
|
<td>Category</td>
|
||||||
|
<td>Score</td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td colspan="2">Percentage</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for score in subject.list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{score.component.due_date}}</td>
|
||||||
|
<td><a href="{% url 'component-detail' score.component.subject.pk score.component.pk %}">{{score.component}}</a></td>
|
||||||
|
<td>{{score.component.get_category_display}}</td>
|
||||||
|
<td>{{score.value}}</td>
|
||||||
|
<td>{{score.component.grade_total}}</td>
|
||||||
|
<td>{{score.grade_as_percentage}}%</td>
|
||||||
|
<td><a href="{% url 'score-update' score.pk %}?return_to={{request.get_full_path}}">Change score</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% empty %}
|
||||||
|
<p>No components graded yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Attendence</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% regroup entry_list by get_status_display as rentry_list %}
|
||||||
|
{% for status in rentry_list %}
|
||||||
|
<h4>{{ status.grouper }}</h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Date</td>
|
||||||
|
<td colspan="2">Status</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in status.list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{entry.day.date}}</td>
|
||||||
|
<td><a href="{% url 'entry-update' entry.pk %}">Update</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% empty %}
|
||||||
|
<p>No attendence taken yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
12
src/core/templates/core/student_form.html
Normal file
12
src/core/templates/core/student_form.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Update Student</h1>
|
||||||
|
<form method="POST" action="{% url 'core:student-update' student.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Save changes"> or <a href="{% url 'core:student-detail' student.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
47
src/core/templates/core/student_list.html
Normal file
47
src/core/templates/core/student_list.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block head_title %}Students | {% endblock head_title %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<menu>
|
||||||
|
<li><strong><a href="{% url 'core:schoolyear-detail' school_year.year %}">{{ school_year.year }}</a></strong></li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="list">
|
||||||
|
<header class="list__header">
|
||||||
|
<div class="list__title">
|
||||||
|
<h1>Students</h1>
|
||||||
|
<a href="{% url 'core:student-create' school_year.year %}" class="action-button">+ New Student</a>
|
||||||
|
</div>
|
||||||
|
<a href="">Student Tags →</a>
|
||||||
|
</header>
|
||||||
|
<table class="list__table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><a href="?order_by=record_num&direction=asc">Student No. ↕</a></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for student in student_list %}
|
||||||
|
<tr class="has-link" onclick="document.location='{% url 'core:student-detail' school_year.year student.pk %}'">
|
||||||
|
<td>{{ student.student_id }}</td>
|
||||||
|
<td>{{ student }}</td>
|
||||||
|
<td>(tags)</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7">No students yet.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% include 'core/partials/pagination.html' %}
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
0
src/core/templatetags/__init__.py
Normal file
0
src/core/templatetags/__init__.py
Normal file
16
src/core/templatetags/helpers.py
Normal file
16
src/core/templatetags/helpers.py
Normal file
@ -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 ''
|
||||||
3
src/core/tests.py
Normal file
3
src/core/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
61
src/core/urls.py
Normal file
61
src/core/urls.py
Normal file
@ -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('<slug:year>/', 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('<int:student_pk>/', 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'
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
])),
|
||||||
|
])),
|
||||||
|
])),
|
||||||
|
]
|
||||||
152
src/core/views.py
Normal file
152
src/core/views.py
Normal file
@ -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')
|
||||||
0
src/gradebook/__init__.py
Normal file
0
src/gradebook/__init__.py
Normal file
12
src/gradebook/admin.py
Normal file
12
src/gradebook/admin.py
Normal file
@ -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)
|
||||||
6
src/gradebook/apps.py
Normal file
6
src/gradebook/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GradebookConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'gradebook'
|
||||||
43
src/gradebook/forms.py
Normal file
43
src/gradebook/forms.py
Normal file
@ -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',
|
||||||
|
)
|
||||||
69
src/gradebook/migrations/0001_initial.py
Normal file
69
src/gradebook/migrations/0001_initial.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/gradebook/migrations/0002_component_finished_grading.py
Normal file
18
src/gradebook/migrations/0002_component_finished_grading.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/gradebook/migrations/__init__.py
Normal file
0
src/gradebook/migrations/__init__.py
Normal file
100
src/gradebook/models.py
Normal file
100
src/gradebook/models.py
Normal file
@ -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})
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{component}}</h1>
|
||||||
|
<form method="post" action="{% url 'component-delete' component.subject.pk component.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{component}}"> or <a href="{% url 'component-detail' component.subject.pk component.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
17
src/gradebook/templates/gradebook/component_create_form.html
Normal file
17
src/gradebook/templates/gradebook/component_create_form.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>{{subject}}</h1>
|
||||||
|
<h2>Create Component</h2>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'component-create' subject.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Create component"> or <a href="{% url 'subject-detail' subject.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
77
src/gradebook/templates/gradebook/component_detail.html
Normal file
77
src/gradebook/templates/gradebook/component_detail.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<header class="generic__header">
|
||||||
|
<h1>{{component.name}}</h1>
|
||||||
|
<a href="{% url 'component-update' component.subject.pk component.pk %}" class="action-button">Update Component</a>
|
||||||
|
</header>
|
||||||
|
{% if component.finished_grading %}
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
<span class="component__grade--graded">✓ Graded</span>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if component.tags.count > 0 %}
|
||||||
|
<section>
|
||||||
|
<span>
|
||||||
|
{% for tag in component.tags.all %}
|
||||||
|
<a class="tag__item" href="{% url 'tag-detail' tag.pk %}">{{tag.name}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
<section>
|
||||||
|
<dl>
|
||||||
|
<dt>Due Date</dt>
|
||||||
|
<dd>{{component.due_date}}</dd>
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>{{component.name}}</dd>
|
||||||
|
<dt>Category</dt>
|
||||||
|
<dd>{{component.get_category_display}}</dd>
|
||||||
|
<dt>Grade Total</dt>
|
||||||
|
<dd>{{component.grade_total}}</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Scores</h3>
|
||||||
|
<p>
|
||||||
|
<a class="action-button" href="{% url 'component-manager' component.subject.pk component.pk %}">Bulk Edit Scores</a>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td colspan="2">Score</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for score in component.score_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'student-detail' score.student.pk %}">{{score.student.student_id}} — {{score.student}}</a></td>
|
||||||
|
<td>{{score.value}}</td>
|
||||||
|
<td><a href="{% url 'score-update' score.pk %}">Edit score</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>Avg Score</strong></td>
|
||||||
|
<td><strong>{{component.grade_avg_pre|floatformat:2}}</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if scoreless %}
|
||||||
|
<section>
|
||||||
|
|
||||||
|
<h3>Scoreless</h3>
|
||||||
|
<ul>
|
||||||
|
{% for student in scoreless %}
|
||||||
|
<li>{{student}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
19
src/gradebook/templates/gradebook/component_form.html
Normal file
19
src/gradebook/templates/gradebook/component_form.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="generic__header">
|
||||||
|
<h1>Update Component</h1>
|
||||||
|
<a class="action-button action-delete" href="{% url 'component-delete' component.subject.pk component.pk %}">Delete Component</a>
|
||||||
|
</div>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'component-update' component.subject.pk component.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'component-detail' component.subject.pk component.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
41
src/gradebook/templates/gradebook/component_manager.html
Normal file
41
src/gradebook/templates/gradebook/component_manager.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load gradebook_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>{{component}}, <em>{{component.subject}}</em></h1>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'component-manager' component.subject.pk component.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<h3>Enter Scores</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Out of:</td>
|
||||||
|
<td>{{component.grade_total}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Student</td>
|
||||||
|
<td>Score</td>
|
||||||
|
{% if formset.errors %}
|
||||||
|
<td>Errors</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for student in student_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{student.full_name}}</td>
|
||||||
|
<td><input type="number" name="student_{{student.pk}}" min="0" max="{{component.grade_total}}" value="{{student.cscore}}"></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'component-detail' component.subject.pk component.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
13
src/gradebook/templates/gradebook/score_confirm_delete.html
Normal file
13
src/gradebook/templates/gradebook/score_confirm_delete.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{score}}</h1>
|
||||||
|
<form method="post" action="{% url 'score-delete' score.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{score}}"> or <a href="{% url 'score-detail' score.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
16
src/gradebook/templates/gradebook/score_create_form.html
Normal file
16
src/gradebook/templates/gradebook/score_create_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Create Score</h1>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'score-create' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Create Score"> or <a href="{% url 'subject-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
19
src/gradebook/templates/gradebook/score_form.html
Normal file
19
src/gradebook/templates/gradebook/score_form.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Update Score</h1>
|
||||||
|
<h2>{{score.component.subject}}— <em>{{score.component.get_category_display}}</em>: {{score.component}}</h2>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'score-update' score.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{next}}">
|
||||||
|
<h3>{{score.student}}</h3>
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'student-detail' score.student.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
83
src/gradebook/templates/gradebook/search_results.html
Normal file
83
src/gradebook/templates/gradebook/search_results.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Search Results</h1>
|
||||||
|
{% if student_list.count > 0 %}
|
||||||
|
<section>
|
||||||
|
<h3>Students</h3>
|
||||||
|
<ul>
|
||||||
|
{% for student in student_list %}
|
||||||
|
<li class="student">
|
||||||
|
<a class="student__li" href="{% url 'student-detail' student.pk %}">{{student.student_id}} — {{student.full_name}}</a>
|
||||||
|
{% if student.sit %}
|
||||||
|
<span class="student__attribute">SIT: {{student.get_sit_display}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if student.iep_behavioral %}
|
||||||
|
<span class="student__attribute">IEP behavioral</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if student.iep_math %}
|
||||||
|
<span class="student__attribute">IEP math</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if student.iep_ela %}
|
||||||
|
<span class="student__attribute">IEP ELA</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if student.parent__count > 0 %}
|
||||||
|
<br>Parents:
|
||||||
|
{% for parent in student.parent_set.all %}
|
||||||
|
<a href="{% url 'parent-detail' parent.pk %}">{{parent.full_name}}</a>{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if component_list.count > 0 %}
|
||||||
|
<section>
|
||||||
|
<h3>Components</h3>
|
||||||
|
<ul class="search__results">
|
||||||
|
<li class="search__result">
|
||||||
|
<strong>Subject</strong>
|
||||||
|
<strong>Component</strong>
|
||||||
|
<strong>Due Date</strong>
|
||||||
|
<strong>Tags</strong>
|
||||||
|
</li>
|
||||||
|
<hr>
|
||||||
|
{% for component in component_list %}
|
||||||
|
<li class="search__result">
|
||||||
|
{{component.subject}}
|
||||||
|
<a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
|
||||||
|
<span>{{component.due_date}}</span>
|
||||||
|
<span>
|
||||||
|
{% for tag in component.tags.all %}
|
||||||
|
<a class="tag__item" href="{% url 'tag-detail' tag.pk %}">{{tag.name}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>No components by that name were found.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if message_list.count > 0 %}
|
||||||
|
<section>
|
||||||
|
<h3>Messages</h3>
|
||||||
|
<ul class="search__results">
|
||||||
|
{% for message in message_list %}
|
||||||
|
<li class="search__mresult">
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'thread-detail' message.thread.pk %}">{{message.thread.subject}}</a><br>
|
||||||
|
{{message.content|truncatewords:25}}<br>
|
||||||
|
<a class="" href="{% url 'thread-detail' message.thread.pk %}">Read more →</a>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p>No components by that name were found.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{subject}} Subject</h1>
|
||||||
|
<form method="post" action="{% url 'subject-delete' subject.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{subject}}"> or <a href="{% url 'subject-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
16
src/gradebook/templates/gradebook/subject_create_form.html
Normal file
16
src/gradebook/templates/gradebook/subject_create_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Create Subject</h1>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'subject-create' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Create Subject"> or <a href="{% url 'subject-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
41
src/gradebook/templates/gradebook/subject_detail.html
Normal file
41
src/gradebook/templates/gradebook/subject_detail.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="generic__header">
|
||||||
|
<h1>{{subject.name}}</h1>
|
||||||
|
<a class="action-button" href="{% url 'subject-update' subject.pk %}">Update subject details</a>
|
||||||
|
</div>
|
||||||
|
{% if subject.description %}
|
||||||
|
<span>{{subject.description}}</span>
|
||||||
|
{% endif %}
|
||||||
|
<section>
|
||||||
|
<h3>Syllabus</h3>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'component-create' subject.pk %}" class="action-button">+ New component</a>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Due Date</td>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>Category</td>
|
||||||
|
<td>Grade Total</td>
|
||||||
|
<td>Avg Score</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for component in subject.component_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{component.due_date}}</td>
|
||||||
|
<td><a href="{% url 'component-detail' subject.pk component.pk %}">{{component.name}}</a></td>
|
||||||
|
<td>{{component.get_category_display}}</td>
|
||||||
|
<td>{{component.grade_total}}</td>
|
||||||
|
<td>{{component.grade_avg_pre|floatformat:2}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
19
src/gradebook/templates/gradebook/subject_form.html
Normal file
19
src/gradebook/templates/gradebook/subject_form.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="generic__header">
|
||||||
|
<h1>Update Subject</h1>
|
||||||
|
<a class="action-button action-delete" href="{% url 'subject-delete' subject.pk %}">Delete subject</a>
|
||||||
|
</div>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'subject-update' subject.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'subject-detail' subject.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
35
src/gradebook/templates/gradebook/subject_list.html
Normal file
35
src/gradebook/templates/gradebook/subject_list.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<header>
|
||||||
|
<h1>Curricula</h1>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<h3>Subjects</h3>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'subject-create' %}" class="action-button">+ New subject</a>
|
||||||
|
</p>
|
||||||
|
{% for subject in subject_list %}
|
||||||
|
<div class="subject__item">
|
||||||
|
<h4><a href="{% url 'subject-detail' subject.pk %}">{{subject.name}}</a></h4>
|
||||||
|
{% if subject.description %}
|
||||||
|
<p>{{subject.description}}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h4>Tags</h4>
|
||||||
|
<p><a href="{% url 'tag-create' %}" class="action-button">+ New tag</a></p>
|
||||||
|
<p>
|
||||||
|
{% for tag in tag_list %}
|
||||||
|
<span class="tag">
|
||||||
|
<a class="tag__item" href="{% url 'tag-detail' tag.pk %}">{{tag.name}}</a>
|
||||||
|
<span class="tag__count">{{tag.component__count}}</span>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
13
src/gradebook/templates/gradebook/tag_confirm_delete.html
Normal file
13
src/gradebook/templates/gradebook/tag_confirm_delete.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Delete {{tag}}</h1>
|
||||||
|
<form method="post" action="{% url 'tag-delete' tag.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<input class="action-button action-delete" type="submit" value="Confirm Delete {{tag}}"> or <a href="{% url 'tag-detail' tag.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
16
src/gradebook/templates/gradebook/tag_create_form.html
Normal file
16
src/gradebook/templates/gradebook/tag_create_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Create Tag</h1>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'tag-create' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Create Tag"> or <a href="{% url 'subject-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
35
src/gradebook/templates/gradebook/tag_detail.html
Normal file
35
src/gradebook/templates/gradebook/tag_detail.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<div class="generic__header">
|
||||||
|
<h1>{{tag.name}}</h1>
|
||||||
|
<a class="action-button" href="{% url 'tag-update' tag.pk %}">Update tag details</a>
|
||||||
|
</div>
|
||||||
|
<section>
|
||||||
|
<h3>Components with this tag</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Due Date</td>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>Category</td>
|
||||||
|
<td>Grade Total</td>
|
||||||
|
<td>Avg Score</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for component in tag.component_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{component.due_date}}</td>
|
||||||
|
<td><strong>{{component.subject}}</strong>: <a href="{% url 'component-detail' tag.pk component.pk %}">{{component.name}}</a></td>
|
||||||
|
<td>{{component.get_category_display}}</td>
|
||||||
|
<td>{{component.grade_total}}</td>
|
||||||
|
<td>{{component.grade_avg_pre|floatformat:2}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
16
src/gradebook/templates/gradebook/tag_form.html
Normal file
16
src/gradebook/templates/gradebook/tag_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Update Tag</h1>
|
||||||
|
<section>
|
||||||
|
<form action="{% url 'tag-update' tag.pk %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p>
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'tag-detail' tag.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
20
src/gradebook/templates/gradebook/tag_list.html
Normal file
20
src/gradebook/templates/gradebook/tag_list.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Tags</h1>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'tag-create' %}" class="action-button">+ New tag</a>
|
||||||
|
</p>
|
||||||
|
<section>
|
||||||
|
<ul>
|
||||||
|
{% for tag in tag_list %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'tag-detail' tag.pk %}">{{tag.name}}</a>
|
||||||
|
<span class="tag__count">{{tag.component__count}}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
0
src/gradebook/templatetags/__init__.py
Normal file
0
src/gradebook/templatetags/__init__.py
Normal file
14
src/gradebook/templatetags/gradebook_filters.py
Normal file
14
src/gradebook/templatetags/gradebook_filters.py
Normal file
@ -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 ''
|
||||||
3
src/gradebook/tests.py
Normal file
3
src/gradebook/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
40
src/gradebook/urls.py
Normal file
40
src/gradebook/urls.py
Normal file
@ -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/<int:pk>/', 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/<int:comp_pk>/', 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/<int:pk>/', 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/<int:pk>/', 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'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
297
src/gradebook/views.py
Normal file
297
src/gradebook/views.py
Normal file
@ -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')
|
||||||
11
src/indici/asgi.py
Normal file
11
src/indici/asgi.py
Normal file
@ -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()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user