Initial commit

This commit is contained in:
Nathan Chapman 2021-01-28 18:12:42 -07:00
commit c434b973cc
45 changed files with 1346 additions and 0 deletions

253
.gitignore vendored Normal file
View File

@ -0,0 +1,253 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# 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/
# 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/
**/migrations/**
!**/migrations
!**/migrations/__init__.py
*.sublime-workspace
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

15
Pipfile Normal file
View File

@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
django-compressor = "*"
qrcode = "*"
pillow = "*"
[dev-packages]
[requires]
python_version = "3.9"

145
Pipfile.lock generated Normal file
View File

@ -0,0 +1,145 @@
{
"_meta": {
"hash": {
"sha256": "35627f97351787e78b306a9071b0db88411b941484359b8622cd032f3f6a64f2"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
"markers": "python_version >= '3.5'",
"version": "==3.3.1"
},
"django": {
"hashes": [
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
],
"index": "pypi",
"version": "==3.1.5"
},
"django-appconf": {
"hashes": [
"sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06",
"sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"
],
"version": "==1.0.4"
},
"django-compressor": {
"hashes": [
"sha256:57ac0a696d061e5fc6fbc55381d2050f353b973fb97eee5593f39247bc0f30af",
"sha256:d2ed1c6137ddaac5536233ec0a819e14009553fee0a869bea65d03e5285ba74f"
],
"index": "pypi",
"version": "==2.4"
},
"pillow": {
"hashes": [
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
"sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded",
"sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
"sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
"sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
"sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a",
"sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e",
"sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378",
"sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17",
"sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c",
"sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913",
"sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7",
"sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0",
"sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820",
"sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba",
"sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2",
"sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b",
"sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9",
"sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234",
"sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d",
"sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5",
"sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206",
"sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9",
"sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
"sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
"sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
"sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7",
"sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
"sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0",
"sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
"sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d",
"sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"
],
"index": "pypi",
"version": "==8.1.0"
},
"pytz": {
"hashes": [
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
],
"version": "==2020.5"
},
"qrcode": {
"hashes": [
"sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5",
"sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"
],
"index": "pypi",
"version": "==6.1"
},
"rcssmin": {
"hashes": [
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
],
"version": "==1.0.6"
},
"rjsmin": {
"hashes": [
"sha256:0ab825839125eaca57cc59581d72e596e58a7a56fbc0839996b7528f0343a0a8",
"sha256:211c2fe8298951663bbc02acdffbf714f6793df54bfc50e1c6c9e71b3f2559a3",
"sha256:466fe70cc5647c7c51b3260c7e2e323a98b2b173564247f9c89e977720a0645f",
"sha256:585e75a84d9199b68056fd4a083d9a61e2a92dfd10ff6d4ce5bdb04bc3bdbfaf",
"sha256:6044ca86e917cd5bb2f95e6679a4192cef812122f28ee08c677513de019629b3",
"sha256:714329db774a90947e0e2086cdddb80d5e8c4ac1c70c9f92436378dedb8ae345",
"sha256:799890bd07a048892d8d3deb9042dbc20b7f5d0eb7da91e9483c561033b23ce2",
"sha256:975b69754d6a76be47c0bead12367a1ca9220d08e5393f80bab0230d4625d1f4",
"sha256:b15dc75c71f65d9493a8c7fa233fdcec823e3f1b88ad84a843ffef49b338ac32",
"sha256:dd0f4819df4243ffe4c964995794c79ca43943b5b756de84be92b445a652fb86",
"sha256:e3908b21ebb584ce74a6ac233bdb5f29485752c9d3be5e50c5484ed74169232c",
"sha256:e487a7783ac4339e79ec610b98228eb9ac72178973e3dee16eba0e3feef25924",
"sha256:ecd29f1b3e66a4c0753105baec262b331bcbceefc22fbe6f7e8bcd2067bcb4d7"
],
"version": "==1.1.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"sqlparse": {
"hashes": [
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.1"
}
},
"develop": {}
}

0
accounts/__init__.py Normal file
View File

6
accounts/admin.py Normal file
View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import Department, Instructor, Student
admin.site.register(Department)
admin.site.register(Instructor)
admin.site.register(Student)

5
accounts/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

3
accounts/forms.py Normal file
View File

@ -0,0 +1,3 @@
from django import forms
from django.contrib.auth.models import User

View File

37
accounts/models.py Normal file
View File

@ -0,0 +1,37 @@
from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
class Department(models.Model):
name = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Instructor(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
department = models.ForeignKey(Department, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse('user-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
student_number = models.IntegerField()
department = models.ForeignKey(Department, on_delete=models.CASCADE)
is_clocked_in = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse('user-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.student_number}: {self.user.first_name} {self.user.last_name}'

View File

@ -0,0 +1,12 @@
{% extends 'application.html' %}
{% block content %}
<h1>User</h1>
<section>
<h3>{{ user.first_name }} {{ user.last_name }}</h3>
<p>{{ user.email }}</p>
<p>
<a href="{% url 'account-update' user.id %}">Update</a>
</p>
</section>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends 'application.html' %}
{% block content %}
<h1>Update User</h1>
<section>
<h3>{{ user.first_name }} {{ user.last_name }}</h3>
{% if user.student %}
<p>Student ID: {{user.student.student_number}}</p>
{% endif %}
<form method="post" action="{% url 'account-update' user.id %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save changes" />
</form>
<p><a href="{% url 'password_change' %}">Update password</a></p>
</section>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'application.html' %}
{% block content %}
<h1>Users</h1>
<section>
<table>
<tbody>
{% for user in user_list %}
<tr>
<td>{{ user.username }}</td>
<td>{{user.first_name}} {{user.last_name}}</td>
<td><a href="{% url 'user-detail' user.id %}">Update</a></td>
</tr>
{% empty %}
<tr><td>No users yet.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}

3
accounts/tests.py Normal file
View File

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

18
accounts/urls.py Normal file
View File

@ -0,0 +1,18 @@
from django.urls import include, path
from . import views
# list/ /users/
# create/ /users/new/
# detail/ /users/1/
# update/ /users/1/update/ (update shift preferences)
# delete/ /users/1/delete/
urlpatterns = [
path('', views.AccountListView.as_view(), name='account-list'),
path('new/', views.AccountCreateView.as_view(), name='account-create'),
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'),
])),
]

36
accounts/views.py Normal file
View File

@ -0,0 +1,36 @@
from django.shortcuts import render, reverse
from django.urls import reverse_lazy
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.models import User
from django.contrib.auth.forms import PasswordChangeForm
class AccountListView(LoginRequiredMixin, ListView):
model = User
template_name = 'accounts/account_list.html'
class AccountCreateView(LoginRequiredMixin, CreateView):
model = User
fields = ['email', 'password']
template_name = 'accounts/account_form.html'
class AccountDetailView(LoginRequiredMixin, DetailView):
model = User
template_name = 'accounts/account_detail.html'
class AccountUpdateView(LoginRequiredMixin, UpdateView):
model = User
# form_class = PasswordChangeForm
fields = ['username', 'email']
template_name = 'accounts/account_form.html'
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse('account-detail', kwargs={'pk': pk})
class AccountDeleteView(LoginRequiredMixin, DeleteView):
model = User
success_url = reverse_lazy('account-list')

0
attendance/__init__.py Normal file
View File

5
attendance/admin.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Code, Period
admin.site.register(Code)
admin.site.register(Period)

5
attendance/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AttendanceConfig(AppConfig):
name = 'attendance'

2
attendance/forms.py Normal file
View File

@ -0,0 +1,2 @@
from django import forms

View File

23
attendance/models.py Normal file
View File

@ -0,0 +1,23 @@
from django.db import models
from accounts.models import Student
class Code(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
qr_code = models.ImageField(upload_to='qr_codes', blank=True)
def __str__(self):
return str(self.name)
class Period(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
clocked_in = models.DateTimeField(auto_now_add=True)
clocked_out = models.DateTimeField(blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
def duration(self):
return round(self.clocked_out - self.clocked_in)
def __str__(self):
return f'{self.clocked_in}: {self.student.user.first_name} {self.student.user.last_name}'

View File

@ -0,0 +1 @@
attendance_detail

View File

@ -0,0 +1,11 @@
{% extends 'application.html' %}
{% block content %}
{% if user.student %}
{% include "attendance/attendance_overview_student.html" %}
{% elif user.instructor %}
{% include "attendance/attendance_overview_instructor.html" %}
{% else %}
<p>You are neither student nor instructor.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,29 @@
<section>
<header>
<p>{{ user.instructor.department.name }}</p>
<a href="">Update profile</a>
</header>
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
</section>
<article>
<h2>Attendance log</h2>
<table>
<tbody>
{% for student in students %}
<tr>
<td>{{ student.user.first_name }}</td>
{% for period in student.period_set.all %}
<td>{{ period.clocked_in }}</td>
{% if period.clocked_out %}
<td>{{ period.clocked_out }}</td>
{% else %}
<td>You have not clocked out.</td>
{% endif %}
{% endfor %}
</tr>
{% empty %}
<tr><td colspan="2">No periods yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>

View File

@ -0,0 +1,33 @@
<section>
<header>
<p>{{ user.student.department.name }}</p>
<a href="{% url 'account-update' user.id %}">Update profile</a>
</header>
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
{% if user.student.is_clocked_in %}
<p>You are currently clocked in.</p>
<a href="">Clock out ></a>
{% else %}
<p>You are not clocked in.</p>
<a href="">Clock in ></a>
{% endif %}
</section>
<article>
<h2>Attendance log</h2>
<table>
<tbody>
{% for period in user.student.period_set.all %}
<tr>
<td>{{ period.clocked_in }}</td>
{% if period.clocked_out %}
<td>{{ period.clocked_out }}</td>
{% else %}
<td>You have not clocked out.</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="2">No periods yet.</td></tr>
{% endfor %}
</tbody>
</table>
</article>

3
attendance/tests.py Normal file
View File

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

19
attendance/urls.py Normal file
View File

@ -0,0 +1,19 @@
from django.urls import include, path
from . import views
# list/ /users/
# create/ /users/new/
# detail/ /users/1/
# update/ /users/1/update/ (update shift preferences)
# delete/ /users/1/delete/
urlpatterns = [
path('', views.AttendanceOverview.as_view(), name='attendance-overview'),
path('new/', views.AttendanceCreateView.as_view(), name='attendance-create'),
path('<int:pk>/', include([
path('', views.AttendanceDetailView.as_view(), name='attendance-detail'),
path('update/', views.AttendanceUpdateView.as_view(), name='attendance-update'),
path('delete/', views.AttendanceDeleteView.as_view(), name='attendance-delete'),
])),
]

50
attendance/views.py Normal file
View File

@ -0,0 +1,50 @@
from django.shortcuts import render, reverse
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, PermissionRequiredMixin
from django.contrib.auth.models import User
from accounts.models import Instructor, Student
from .models import Period
# EXAMPLE PERMISSION MIXIN
# class MyView(PermissionRequiredMixin, View):
# permission_required = 'polls.add_choice'
# # Or multiple of permissions:
# permission_required = ('polls.view_choice', 'polls.change_choice')
class AttendanceOverview(LoginRequiredMixin, TemplateView):
template_name = 'attendance/attendance_overview.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = User.objects.get(pk=self.request.user.id)
if hasattr(self.request.user, 'instructor'):
context['students'] = Student.objects.filter(department=self.request.user.instructor.department)
context['period_list'] = Period.objects.all()
return context
class AttendanceCreateView(LoginRequiredMixin, CreateView):
model = Period
fields = ['email', 'password']
template_name = 'attendance/attendance_form.html'
class AttendanceDetailView(LoginRequiredMixin, DetailView):
model = Period
template_name = 'attendance/attendance_detail.html'
class AttendanceUpdateView(LoginRequiredMixin, UpdateView):
model = Period
fields = ['username', 'email']
template_name = 'attendance/attendance_form.html'
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse('attendance-detail', kwargs={'pk': pk})
class AttendanceDeleteView(LoginRequiredMixin, DeleteView):
model = Period
success_url = reverse_lazy('account-overview')

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tracker.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

215
static/css/base.css Normal file
View File

@ -0,0 +1,215 @@
/*bg (gray) white
primary (black) #323834
faded (light-black) #747a7c
accent 1 (red) #2980B9
accent 2 (orange) #9b6f45
accent 3 (blue) #242e34
*/
@import url('https://fonts.googleapis.com/css2?family=Heebo:wght@400;700;900&display=swap');
html {
font-size: 1.125em;
}
body {
max-width: 8.5in;
margin: 0.25in auto;
background-color: white;
color: #323834;
font-family: 'Heebo', sans-serif;
font-weight: 400;
line-height: 1.65;
}
header {
display: flex;
align-items: baseline;
justify-content: space-between;
}
/* Text Elements */
p {
margin-bottom: 1.15rem;
}
h1, h2, h3, h4, h5 {
/*margin: 2.75rem 0 1.05rem;*/
margin: 0 0 1.05rem;
font-weight: 800;
line-height: 1.15;
}
h1 {
margin-top: 0;
font-size: 2.488em;
}
h2 {
font-size: 2.074em;
}
h3 {
font-size: 1.728em;
}
h4 {
font-size: 1.44em;
}
h5 {
font-size: 1.2em;
}
small {
font-size: 0.833em;
}
a {
color: #2980B9;
cursor: pointer;
white-space: nowrap;
}
small {
font-size: 0.8em;
}
hr {
margin: 0;
border: 0.8px solid #bdc3c7;
}
blockquote {
text-align: left;
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
margin: 1.5em 0;
padding: 0 15px;
font-family: serif;
}
/* Forms */
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0;
max-width: 100%;
}
input {
text-align: left;
font-weight: 400;
outline: 0;
}
label {
text-align: left;
font-weight: 700;
}
input[type=text],
input[type=email],
input[type=password],
select[multiple=multiple],
textarea,
.action-button,
.datepickr {
text-align: left;
color: #2c3e50;
height: 2.75em;
border: 4px solid #bdc3c7;
padding: 1em;
width: 100%;
outline: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
input[type=submit],
.action-button {
font-weight: 900;
padding: 0.5em 1em;
border: none;
background-color: #2980B9;
color: #ffffff;
cursor: pointer;
text-decoration: none;
box-shadow: 0 4px 0 #2980B9;
}
input[type=submit]:active,
.action-button:active {
box-shadow: none;
}
input:focus,
textarea:focus {
border-color: #2980B9;
}
select[multiple=multiple] {
height: 125px;
}
input[type=number] {
max-width: 150px;
height: 3.32em;
}
input[type=checkbox] {
width: 1em;
vertical-align: text-top;
}
textarea {
width: 100%;
height: 100px;
line-height: 1.45;
}
::-webkit-input-placeholder {
font-style: oblique;
}
::-moz-input-placeholder {
font-style: oblique;
}
::-ms-input-placeholder {
font-style: oblique;
}
section {
padding: 1em;
border: 1px solid #bdc3c7;
box-shadow: 0 0 4px #bdc3c7;
margin-bottom: 1.3em;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
margin-bottom: 1.3em;
border: 1px solid #bdc3c7;
}
table a {
white-space: normal;
}
td,
th {
text-align: left;
font-size: 0.85em;
padding: 2px 10px;
border-bottom: 1px solid;
border-color: #bdc3c7;
}

View File

@ -0,0 +1,41 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tracker</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
{% block head %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}">
</head>
<body>
<header>
<h4>Tracker</h4>
{% if user.is_authenticated %}
<nav>
<a class="logout" href="{% url 'logout' %}">Logout</a>
</nav>
{% else %}
<nav>
<a class="login" href="{% url 'login' %}">Login</a>
</nav>
{% endif %}
</header>
<aside>
{% block aside %}
{% endblock %}
</aside>
<main>
{% block content %}
{% endblock %}
</main>
<footer>
</footer>
</body>
</html>

View File

@ -0,0 +1,7 @@
{% extends 'application.html' %}
{% block content %}
<section>
<p>You have been logged out. <a href="{% url 'login' %}">Log in</a></p>
</section>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'application.html' %}
{% block content %}
<section>
<h1>Log in</h1>
{% if form.errors %}
<p class="error">Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<p>
{{ form.username.label_tag }}
<br>
{{ form.username }}
</p>
<p>
{{ form.password.label_tag }}
<br>
{{ form.password }}
<small>
Forgot your password? <a href="{% url 'password_reset' %}">Reset your password here</a>.
</small>
</p>
<p>
<input type="submit" value="Login">
<input type="hidden" name="next" value="{{ next }}">
</p>
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'application.html' %}
{% block content %}
<section>
<p>Password has been changed. <a href="{% url 'login' %}">Log in</a></p>
</section>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'application.html' %}
{% block content %}
<section>
<form method="post" action="{% url 'password_change' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Change my password" />
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'application.html' %}
{% block content %}
<sectin>
<p>Password was reset successfully.</p>
</section>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends 'application.html' %}
{% block content %}
{% if validlink %}
<section>
<h1>Reset password</h1>
<p>Enter a <em>new</em> password below.</p>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Reset your password" />
</form>
</section>
{% else %}
<section>
<p>Password reset failed</p>
</section>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'application.html' %}
{% block content %}
<section>
<p>An email with password reset instructions has been sent.</p>
</section>
{% endblock %}

View File

@ -0,0 +1,5 @@
<h1>Password Reset</h1>
<p>
<strong>Reset your password at {{ site_name }}</strong>:<br>
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
</p>

View File

@ -0,0 +1,17 @@
{% extends 'application.html' %}
{% block content %}
<section>
<h1>Reset your password</h1>
<p>Enter your email address below and we'll send you instructions.</p>
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Send me instructions">
</p>
</form>
</section>
{% endblock %}

0
tracker/__init__.py Normal file
View File

16
tracker/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for tracker project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tracker.settings')
application = get_asgi_application()

140
tracker/settings.py Normal file
View File

@ -0,0 +1,140 @@
"""
Django settings for tracker project.
Generated by 'django-admin startproject' using Django 3.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '$i_349k*ollxqq9nwase3!da)eui(3&^!-_^05_w$%6uc!eclp'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'compressor',
'accounts.apps.AccountsConfig',
'attendance.apps.AttendanceConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'tracker.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'tracker.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'US/Mountain'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other finders..
'compressor.finders.CompressorFinder',
)
STATIC_URL = '/static/'
STATICFILES_DIRS = ['static']
STATIC_ROOT = '/var/www/static/'
# Media file storage
MEDIA_URL = '/media/'
MEDIA_ROOT = [BASE_DIR / 'media']
LOGIN_REDIRECT_URL = '/attendance'

24
tracker/urls.py Normal file
View File

@ -0,0 +1,24 @@
"""tracker URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('accounts.urls')),
path('attendance/', include('attendance.urls')),
path('admin/', admin.site.urls),
]

16
tracker/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for tracker project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tracker.settings')
application = get_wsgi_application()