Initial commit

This commit is contained in:
Nathan Chapman 2022-06-30 15:12:40 -06:00
commit cd502df19a
159 changed files with 6152 additions and 0 deletions

130
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

5
src/accounts/admin.py Normal file
View 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
View 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
View 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',
]

View 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()),
],
),
]

View File

27
src/accounts/models.py Normal file
View 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
)

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
src/accounts/urls.py Normal file
View 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
View 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')

View File

6
src/attendance/admin.py Normal file
View 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
View 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
View 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()
}

View 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',),
},
),
]

View File

33
src/attendance/models.py Normal file
View 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}"

View 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 %}

View 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 %}

View 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 %}

View 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">&laquo; first</a>
<a class="minor-action-button" href="?page={{ page_obj.previous_page_number }}">&lsaquo; 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 &rsaquo;</a>
<a class="minor-action-button" href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</section>
</article>
{% endblock %}

View File

@ -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 %}

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
src/attendance/urls.py Normal file
View 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
View 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
View File

3
src/core/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
src/core/apps.py Normal file
View 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
View 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',
}

View 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'),
),
]

View 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),
),
]

View File

@ -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,
),
]

View 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),
),
]

View 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),
),
]

View 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'],
},
),
]

View File

259
src/core/models.py Normal file
View 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']

View File

@ -0,0 +1,17 @@
<section class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</section>

View 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 %}

View 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 %}

View 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>&rarr;</span>
</a>
<a href="" class="tool">
<h3>Attendence</h3>
<p>Tool Description</p>
<span>&rarr;</span>
</a>
<a href="" class="tool">
<h3>Subjects</h3>
<p>Tool Description</p>
<span>&rarr;</span>
</a>
</div>
</article>
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 }} &ensp; / &ensp; {{ 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 %}

View 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 %}

View 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 &rarr;</a>
</header>
<table class="list__table">
<thead>
<tr>
<th><a href="?order_by=record_num&direction=asc">Student No. &varr;</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 %}

View File

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

61
src/core/urls.py Normal file
View 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
View 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')

View File

12
src/gradebook/admin.py Normal file
View 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
View 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
View 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',
)

View 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'),
),
]

View 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),
),
]

View File

100
src/gradebook/models.py Normal file
View 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})

View File

@ -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 %}

View 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 %}

View 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}} &mdash; {{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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<article class="panel">
<h1>Update Score</h1>
<h2>{{score.component.subject}}&mdash; <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 %}

View 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}} &mdash; {{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 &rarr;</a>
</p>
</li>
{% empty %}
<p>No components by that name were found.</p>
{% endfor %}
</ul>
</section>
{% endif %}
</article>
{% endblock %}

View File

@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

40
src/gradebook/urls.py Normal file
View 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
View 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
View 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