From c434b973cc7395f2de81db600bbde5ac6abecea1 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Thu, 28 Jan 2021 18:12:42 -0700 Subject: [PATCH] Initial commit --- .gitignore | 253 ++++++++++++++++++ Pipfile | 15 ++ Pipfile.lock | 145 ++++++++++ accounts/__init__.py | 0 accounts/admin.py | 6 + accounts/apps.py | 5 + accounts/forms.py | 3 + accounts/migrations/__init__.py | 0 accounts/models.py | 37 +++ .../templates/accounts/account_detail.html | 12 + accounts/templates/accounts/account_form.html | 19 ++ accounts/templates/accounts/account_list.html | 20 ++ accounts/tests.py | 3 + accounts/urls.py | 18 ++ accounts/views.py | 36 +++ attendance/__init__.py | 0 attendance/admin.py | 5 + attendance/apps.py | 5 + attendance/forms.py | 2 + attendance/migrations/__init__.py | 0 attendance/models.py | 23 ++ .../attendance/attendance_detail.html | 1 + .../attendance/attendance_overview.html | 11 + .../attendance_overview_instructor.html | 29 ++ .../attendance_overview_student.html | 33 +++ attendance/tests.py | 3 + attendance/urls.py | 19 ++ attendance/views.py | 50 ++++ manage.py | 22 ++ static/css/base.css | 215 +++++++++++++++ templates/application.html | 41 +++ templates/registration/logged_out.html | 7 + templates/registration/login.html | 33 +++ .../registration/password_change_done.html | 7 + .../registration/password_change_form.html | 12 + .../registration/password_reset_complete.html | 7 + .../registration/password_reset_confirm.html | 24 ++ .../registration/password_reset_done.html | 7 + .../registration/password_reset_email.html | 5 + .../registration/password_reset_form.html | 17 ++ tracker/__init__.py | 0 tracker/asgi.py | 16 ++ tracker/settings.py | 140 ++++++++++ tracker/urls.py | 24 ++ tracker/wsgi.py | 16 ++ 45 files changed, 1346 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/forms.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/templates/accounts/account_detail.html create mode 100644 accounts/templates/accounts/account_form.html create mode 100644 accounts/templates/accounts/account_list.html create mode 100644 accounts/tests.py create mode 100644 accounts/urls.py create mode 100644 accounts/views.py create mode 100644 attendance/__init__.py create mode 100644 attendance/admin.py create mode 100644 attendance/apps.py create mode 100644 attendance/forms.py create mode 100644 attendance/migrations/__init__.py create mode 100644 attendance/models.py create mode 100644 attendance/templates/attendance/attendance_detail.html create mode 100644 attendance/templates/attendance/attendance_overview.html create mode 100644 attendance/templates/attendance/attendance_overview_instructor.html create mode 100644 attendance/templates/attendance/attendance_overview_student.html create mode 100644 attendance/tests.py create mode 100644 attendance/urls.py create mode 100644 attendance/views.py create mode 100755 manage.py create mode 100644 static/css/base.css create mode 100644 templates/application.html create mode 100644 templates/registration/logged_out.html create mode 100644 templates/registration/login.html create mode 100644 templates/registration/password_change_done.html create mode 100644 templates/registration/password_change_form.html create mode 100644 templates/registration/password_reset_complete.html create mode 100644 templates/registration/password_reset_confirm.html create mode 100644 templates/registration/password_reset_done.html create mode 100644 templates/registration/password_reset_email.html create mode 100644 templates/registration/password_reset_form.html create mode 100644 tracker/__init__.py create mode 100644 tracker/asgi.py create mode 100644 tracker/settings.py create mode 100644 tracker/urls.py create mode 100644 tracker/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c438cb --- /dev/null +++ b/.gitignore @@ -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.* diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..26c3d3c --- /dev/null +++ b/Pipfile @@ -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" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..7d124a4 --- /dev/null +++ b/Pipfile.lock @@ -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": {} +} diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..179ba7b --- /dev/null +++ b/accounts/admin.py @@ -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) diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..af397a2 --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,3 @@ +from django import forms +from django.contrib.auth.models import User + diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..d06bcf1 --- /dev/null +++ b/accounts/models.py @@ -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}' diff --git a/accounts/templates/accounts/account_detail.html b/accounts/templates/accounts/account_detail.html new file mode 100644 index 0000000..025a013 --- /dev/null +++ b/accounts/templates/accounts/account_detail.html @@ -0,0 +1,12 @@ +{% extends 'application.html' %} + +{% block content %} +

User

+
+

{{ user.first_name }} {{ user.last_name }}

+

{{ user.email }}

+

+ Update +

+
+{% endblock %} diff --git a/accounts/templates/accounts/account_form.html b/accounts/templates/accounts/account_form.html new file mode 100644 index 0000000..3706fc9 --- /dev/null +++ b/accounts/templates/accounts/account_form.html @@ -0,0 +1,19 @@ +{% extends 'application.html' %} + +{% block content %} +

Update User

+ +
+

{{ user.first_name }} {{ user.last_name }}

+ {% if user.student %} +

Student ID: {{user.student.student_number}}

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

Update password

+
+{% endblock %} diff --git a/accounts/templates/accounts/account_list.html b/accounts/templates/accounts/account_list.html new file mode 100644 index 0000000..0008997 --- /dev/null +++ b/accounts/templates/accounts/account_list.html @@ -0,0 +1,20 @@ +{% extends 'application.html' %} + +{% block content %} +

Users

+
+ + + {% for user in user_list %} + + + + + + {% empty %} + + {% endfor %} + +
{{ user.username }}{{user.first_name}} {{user.last_name}}Update
No users yet.
+
+{% endblock %} diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..e0b42e8 --- /dev/null +++ b/accounts/urls.py @@ -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('/', include([ + path('', views.AccountDetailView.as_view(), name='account-detail'), + path('update/', views.AccountUpdateView.as_view(), name='account-update'), + path('delete/', views.AccountDeleteView.as_view(), name='account-delete'), + ])), +] diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..7cfd233 --- /dev/null +++ b/accounts/views.py @@ -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') diff --git a/attendance/__init__.py b/attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/attendance/admin.py b/attendance/admin.py new file mode 100644 index 0000000..f003c9b --- /dev/null +++ b/attendance/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Code, Period + +admin.site.register(Code) +admin.site.register(Period) diff --git a/attendance/apps.py b/attendance/apps.py new file mode 100644 index 0000000..177ba15 --- /dev/null +++ b/attendance/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AttendanceConfig(AppConfig): + name = 'attendance' diff --git a/attendance/forms.py b/attendance/forms.py new file mode 100644 index 0000000..0afb906 --- /dev/null +++ b/attendance/forms.py @@ -0,0 +1,2 @@ +from django import forms + diff --git a/attendance/migrations/__init__.py b/attendance/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/attendance/models.py b/attendance/models.py new file mode 100644 index 0000000..d49997c --- /dev/null +++ b/attendance/models.py @@ -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}' diff --git a/attendance/templates/attendance/attendance_detail.html b/attendance/templates/attendance/attendance_detail.html new file mode 100644 index 0000000..a3fb2c2 --- /dev/null +++ b/attendance/templates/attendance/attendance_detail.html @@ -0,0 +1 @@ +attendance_detail diff --git a/attendance/templates/attendance/attendance_overview.html b/attendance/templates/attendance/attendance_overview.html new file mode 100644 index 0000000..bc8a81a --- /dev/null +++ b/attendance/templates/attendance/attendance_overview.html @@ -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 %} +

You are neither student nor instructor.

+{% endif %} +{% endblock %} diff --git a/attendance/templates/attendance/attendance_overview_instructor.html b/attendance/templates/attendance/attendance_overview_instructor.html new file mode 100644 index 0000000..42e96e5 --- /dev/null +++ b/attendance/templates/attendance/attendance_overview_instructor.html @@ -0,0 +1,29 @@ +
+
+

{{ user.instructor.department.name }}

+ Update profile +
+

{{ user.first_name }} {{ user.last_name }}

+
+
+

Attendance log

+ + + {% for student in students %} + + + {% for period in student.period_set.all %} + + {% if period.clocked_out %} + + {% else %} + + {% endif %} + {% endfor %} + + {% empty %} + + {% endfor %} + +
{{ student.user.first_name }}{{ period.clocked_in }}{{ period.clocked_out }}You have not clocked out.
No periods yet.
+
diff --git a/attendance/templates/attendance/attendance_overview_student.html b/attendance/templates/attendance/attendance_overview_student.html new file mode 100644 index 0000000..46e5eaf --- /dev/null +++ b/attendance/templates/attendance/attendance_overview_student.html @@ -0,0 +1,33 @@ +
+
+

{{ user.student.department.name }}

+ Update profile +
+

{{ user.first_name }} {{ user.last_name }}

+ {% if user.student.is_clocked_in %} +

You are currently clocked in.

+ Clock out > + {% else %} +

You are not clocked in.

+ Clock in > + {% endif %} +
+
+

Attendance log

+ + + {% for period in user.student.period_set.all %} + + + {% if period.clocked_out %} + + {% else %} + + {% endif %} + + {% empty %} + + {% endfor %} + +
{{ period.clocked_in }}{{ period.clocked_out }}You have not clocked out.
No periods yet.
+
diff --git a/attendance/tests.py b/attendance/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/attendance/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/attendance/urls.py b/attendance/urls.py new file mode 100644 index 0000000..316ce1d --- /dev/null +++ b/attendance/urls.py @@ -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('/', 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'), + ])), +] + diff --git a/attendance/views.py b/attendance/views.py new file mode 100644 index 0000000..3a73765 --- /dev/null +++ b/attendance/views.py @@ -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') diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..2eaaf17 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', '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() diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..e453550 --- /dev/null +++ b/static/css/base.css @@ -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; +} diff --git a/templates/application.html b/templates/application.html new file mode 100644 index 0000000..c845155 --- /dev/null +++ b/templates/application.html @@ -0,0 +1,41 @@ +{% load static %} + + + + Tracker + + + + + {% block head %} + {% endblock %} + + + +
+

Tracker

+ {% if user.is_authenticated %} + + {% else %} + + {% endif %} +
+ + + +
+ {% block content %} + {% endblock %} +
+ +
+
+ + diff --git a/templates/registration/logged_out.html b/templates/registration/logged_out.html new file mode 100644 index 0000000..4877771 --- /dev/null +++ b/templates/registration/logged_out.html @@ -0,0 +1,7 @@ +{% extends 'application.html' %} + +{% block content %} +
+

You have been logged out. Log in

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

Log in

+ {% if form.errors %} +

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

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

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

+ +

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

+ +

+ + +

+
+
+{% endblock %} diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html new file mode 100644 index 0000000..fb7ce79 --- /dev/null +++ b/templates/registration/password_change_done.html @@ -0,0 +1,7 @@ +{% extends 'application.html' %} + +{% block content %} +
+

Password has been changed. Log in

+
+{% endblock %} diff --git a/templates/registration/password_change_form.html b/templates/registration/password_change_form.html new file mode 100644 index 0000000..a4da103 --- /dev/null +++ b/templates/registration/password_change_form.html @@ -0,0 +1,12 @@ +{% extends 'application.html' %} + +{% block content %} +
+
+ {% csrf_token %} + {{ form.as_p }} + + +
+
+{% endblock %} diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..151d61c --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,7 @@ +{% extends 'application.html' %} + +{% block content %} + +

Password was reset successfully.

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

Reset password

+

Enter a new password below.

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

Password reset failed

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

An email with password reset instructions has been sent.

+
+{% endblock %} diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html new file mode 100644 index 0000000..870e7e8 --- /dev/null +++ b/templates/registration/password_reset_email.html @@ -0,0 +1,5 @@ +

Password Reset

+

+ Reset your password at {{ site_name }}:
+ {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} +

diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100644 index 0000000..d007a97 --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,17 @@ +{% extends 'application.html' %} + +{% block content %} +
+

Reset your password

+

Enter your email address below and we'll send you instructions.

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

+ +

+
+
+{% endblock %} diff --git a/tracker/__init__.py b/tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tracker/asgi.py b/tracker/asgi.py new file mode 100644 index 0000000..a923baf --- /dev/null +++ b/tracker/asgi.py @@ -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() diff --git a/tracker/settings.py b/tracker/settings.py new file mode 100644 index 0000000..c038c9f --- /dev/null +++ b/tracker/settings.py @@ -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' diff --git a/tracker/urls.py b/tracker/urls.py new file mode 100644 index 0000000..6a110ff --- /dev/null +++ b/tracker/urls.py @@ -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), +] diff --git a/tracker/wsgi.py b/tracker/wsgi.py new file mode 100644 index 0000000..db9818e --- /dev/null +++ b/tracker/wsgi.py @@ -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()