Merge branch 'master' into develop

This commit is contained in:
Nathan Chapman 2021-01-30 17:45:35 -07:00
commit 863f3c061e
26 changed files with 346 additions and 55 deletions

View File

@ -19,7 +19,7 @@ class Instructor(models.Model):
modified = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse('user-detail', kwargs={'pk': self.pk})
return reverse('account-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'
@ -29,12 +29,13 @@ class Student(models.Model):
student_number = models.IntegerField()
department = models.ForeignKey(Department, on_delete=models.CASCADE)
is_clocked_in = models.BooleanField(default=False)
current_period_id = models.IntegerField(blank=True, null=True)
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})
return reverse('account-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.student_number}: {self.user.first_name} {self.user.last_name}'

View File

@ -9,7 +9,7 @@
<tr>
<td>{{ user.username }}</td>
<td>{{user.first_name}} {{user.last_name}}</td>
<td><a href="{% url 'user-detail' user.id %}">Update</a></td>
<td><a href="{% url 'account-detail' user.id %}">Update</a></td>
</tr>
{% empty %}
<tr><td>No users yet.</td></tr>

View File

@ -1,3 +1,108 @@
from django.test import TestCase
import json
from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone
from django.contrib.auth.models import User
from .models import Department, Instructor, Student
# Create your tests here.
class TestModels(TestCase):
def setUp(self):
self.peter = User.objects.create_user('Peter Templer', 'peter@testing.com', 'peterspassword321')
self.nick = User.objects.create_user('Nick Jenkins', 'nick@testing.com', 'nickspassword321')
self.department = Department.objects.create(name = "Blandings Castle")
self.student = Student.objects.create(
user = self.peter,
student_number = 357950,
department = self.department,
)
self.instructor = Instructor.objects.create(
user = self.nick,
department = self.department,
)
def test_student_user_relationship(self):
self.assertEquals(self.peter, self.student.user)
def test_student_get_absolute_url(self):
url = self.student.get_absolute_url()
self.assertEquals(url, '/accounts/1/')
def test_instructor_user_relationship(self):
self.assertEquals(self.nick, self.instructor.user)
def test_instructor_get_absolute_url(self):
url = self.instructor.get_absolute_url()
self.assertEquals(url, '/accounts/1/')
class TestViews(TestCase):
def setUp(self):
self.client = Client()
self.reverse_urls = {
'list': reverse('account-list'),
'create': reverse('account-create'),
'detail': reverse('account-detail', args=['1']),
'update': reverse('account-update', args=['1']),
'delete': reverse('account-delete', args=['1'])
}
self.peter = User.objects.create_user('Peter Templer', 'peter@testing.com', 'peterspassword321')
self.nick = User.objects.create_user('Nick Jenkins', 'nick@testing.com', 'nickspassword321')
self.department = Department.objects.create(name = "Blandings Castle")
self.student = Student.objects.create(
user = self.peter,
student_number = 357950,
department = self.department,
)
self.instructor = Instructor.objects.create(
user = self.nick,
department = self.department,
)
self.client.force_login(self.peter)
def test_students_list(self):
response = self.client.get(self.reverse_urls['list'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'accounts/account_list.html')
def test_students_create_GET(self):
response = self.client.get(self.reverse_urls['create'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'accounts/account_form.html')
def test_students_create_POST(self):
response = self.client.post(self.reverse_urls['create'], {
'user': self.peter.id,
'student_number': 357950,
'department': self.department,
}, follow=True)
self.assertRedirects(
response,
reverse('account-detail', args=[self.peter.id]),
status_code=302,
target_status_code=200,
fetch_redirect_response=True
)
def test_students_detail(self):
response = self.client.get(self.reverse_urls['detail'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'accounts/account_detail.html')
def test_students_update_GET(self):
response = self.client.get(self.reverse_urls['update'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'accounts/account_form.html')
def test_students_update_POST(self):
pass
def test_student_delete(self):
response = self.client.get(self.reverse_urls['delete'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'accounts/account_confirm_delete.html')
def test_student_delete(self):
response = self.client.delete(self.reverse_urls['delete'], json.dumps({ 'id': 1 }))
self.assertEquals(response.status_code, 302)

View File

@ -7,3 +7,20 @@ class AttendanceUpdateForm(forms.Form):
max_length=100,
widget=forms.TextInput(attrs={'autofocus': True})
)
class PeriodForm(forms.ModelForm):
class Meta:
model = Period
fields = ['student', 'station_number', 'clocked_in', 'clocked_out']
widgets = {
'clocked_in': forms.DateTimeInput(format = '%Y-%m-%d %H:%M', attrs = {
'placeholder': '2006-10-25 14:30'
}),
'clocked_out': forms.DateTimeInput(format = '%Y-%m-%d %H:%M', attrs = {
'placeholder': '2006-10-25 14:30'
}),
}
labels = {
'clocked_in': 'Clocked in (example 2006-10-25 14:30):',
'clocked_out': 'Clocked out (example 2006-10-25 14:30):',
}

View File

@ -1,3 +1,4 @@
from datetime import datetime
from django.db import models
from django.urls import reverse
from accounts.models import Student
@ -20,7 +21,7 @@ class Code(models.Model):
class Period(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
clocked_in = models.DateTimeField(auto_now_add=True)
clocked_in = models.DateTimeField()
clocked_out = models.DateTimeField(blank=True, null=True)
station_number = models.IntegerField()
duration = models.DurationField(blank=True, null=True)
@ -38,3 +39,8 @@ class Period(models.Model):
def __str__(self):
return f'{self.clocked_in}: {self.student.user.first_name} {self.student.user.last_name}'
def save(self, *args, **kwargs):
if isinstance(self.clocked_out, datetime):
self.duration = self.clocked_out - self.clocked_in
super(Period, self).save(*args, **kwargs)

View File

@ -7,6 +7,9 @@
</header>
<h3>Active sessions</h3>
<p>
<a href="{% url 'period-create' %}">+ Add new session</a>
</p>
<table>
<thead>
<tr>

View File

@ -1,13 +1,17 @@
<section>
<header>
<header class="header">
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
<a href="{% url 'account-update' user.id %}">Update profile</a>
</header>
<h5>{{ user.student.department.name }}</h5>
{% if user.student.is_clocked_in %}
{% if user.student.is_clocked_in and user.student.code_set.last %}
<p>You are currently clocked in.<br>
Current sesson: {{ period_list.first.clocked_in|timesince }}</p>
Current sesson: {{ current_period.clocked_in|timesince }}</p>
<a href="{% url 'code-update' user.student.code_set.last.id %}">Clock out →</a>
{% elif user.student.is_clocked_in and not user.student.code_set.last %}
<p><span class="message info">You are currently clocked in.</span><br>
Current sesson: <strong>{{ current_period.clocked_in|timesince }}</strong></p>
<a href="{% url 'code-create' %}">Clock out →</a>
{% elif user.student.code_set.last %}
<p>You are not clocked in.</p>
<a href="{% url 'code-update' user.student.code_set.last.id %}">Clock in →</a>

View File

@ -8,7 +8,8 @@
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Generate code" />
<input type="submit" value="Generate code"> or
<a href="{{request.META.HTTP_REFERER}}">Cancel</a>
</form>
</section>
{% endblock %}

View File

@ -19,6 +19,8 @@
<dd>{{ period.clocked_in }}</dd>
<dt>Clocked out</dt>
<dd>{{ period.clocked_out }}</dd>
<dt>Duration</dt>
<dd>{{ period.duration }}</dd>
</dl>
</section>
{% endblock %}

View File

@ -4,11 +4,12 @@
<h1>Generate Period</h1>
<section>
<form method="post" action="{% url 'period-update' period.id %}">
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save changes" />
<input type="submit" value="Save changes" /> or
<a href="{{request.META.HTTP_REFERER}}">Cancel</a>
</form>
</section>
{% endblock %}

View File

@ -1,3 +1,104 @@
from django.test import TestCase
from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone
from django.contrib.auth.models import User
from accounts.models import Department, Instructor, Student
from .models import Code, Period
from .forms import AttendanceUpdateForm
# Code
# student
# station_number
# Period
# student
# station_number
class CodeModelTests(TestCase):
def setUp(self):
self.peter = User.objects.create_user('Peter Templer', 'peter@testing.com', 'peterspassword321')
self.nick = User.objects.create_user('Nick Jenkins', 'nick@testing.com', 'nickspassword321')
self.department = Department.objects.create(name = "Blandings Castle")
self.student = Student.objects.create(
user = self.peter,
student_number = 357950,
department = self.department,
)
self.instructor = Instructor.objects.create(
user = self.nick,
department = self.department,
)
self.code = Code.objects.create(student=self.student, station_number=22)
def test_code_qr_code_str(self):
"""test_qr_code_str() returns a string joining on ':' and contains student_number"""
self.assertEqual("357950:22", self.code.qr_code_str())
class TestForms(TestCase):
def setUp(self):
pass
def test_attendance_update_form(self):
form = AttendanceUpdateForm(data={
'qr_string': '357950:22'
})
self.assertTrue(form.is_valid())
class TestPeriodViews(TestCase):
def setUp(self):
self.client = Client()
self.reverse_urls = {
# 'list': reverse('period-list'),
# 'create': reverse('period-create'),
'detail': reverse('period-detail', args=['1']),
'update': reverse('period-update', args=['1']),
'delete': reverse('period-delete', args=['1'])
}
self.peter = User.objects.create_user('Peter Templer', 'peter@testing.com', 'peterspassword321')
self.nick = User.objects.create_user('Nick Jenkins', 'nick@testing.com', 'nickspassword321')
self.department = Department.objects.create(name = "Blandings Castle")
self.student = Student.objects.create(
user = self.peter,
student_number = 357950,
department = self.department,
)
self.instructor = Instructor.objects.create(
user = self.nick,
department = self.department,
)
self.period = Period.objects.create(
student = self.student,
station_number = 22,
)
self.client.force_login(self.peter)
# def test_period_create_GET(self):
# response = self.client.get(self.reverse_urls['create'])
# self.assertEquals(response.status_code, 200)
# self.assertTemplateUsed(response, 'attendance/period_form.html')
# def test_period_create_POST(self):
# response = self.client.post(self.reverse_urls['create'], {
# 'student': self.student.id,
# 'station_number': 22,
# }, follow=True)
# self.assertRedirects(
# response,
# reverse('period-detail', args=[self.a_shift.id]),
# status_code=302,
# target_status_code=200,
# fetch_redirect_response=True
# )
def test_period_detail(self):
response = self.client.get(self.reverse_urls['detail'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'attendance/period_detail.html')
def test_period_update_GET(self):
response = self.client.get(self.reverse_urls['update'])
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, 'attendance/period_form.html')
# Create your tests here.

View File

@ -10,6 +10,7 @@ from . import views
urlpatterns = [
path('', views.AttendanceOverview.as_view(), name='attendance-overview'),
path('update/', views.AttendanceUpdateView.as_view(), name='attendance-update'),
path('periods/new/', views.PeriodCreateView.as_view(), name='period-create'),
path('periods/<int:pk>/', include([
path('', views.PeriodDetailView.as_view(), name='period-detail'),
path('update/', views.PeriodUpdateView.as_view(), name='period-update'),

View File

@ -12,7 +12,7 @@ from django.contrib import messages
from django.contrib.auth.models import User
from accounts.models import Instructor, Student
from .models import Code, Period
from .forms import AttendanceUpdateForm
from .forms import AttendanceUpdateForm, PeriodForm
# EXAMPLE PERMISSION MIXIN
# class MyView(PermissionRequiredMixin, View):
@ -32,17 +32,18 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
context['period_list'] = Period.objects.order_by('-clocked_in')
elif hasattr(self.request.user, 'student'):
# sum all duration fields for student
total_duration = Period.objects.filter(
periods_duration_sum = Period.objects.filter(
student = self.request.user.student
).filter(
clocked_in__year=timezone.now().year
).filter(
clocked_in__month=timezone.now().month
).order_by('-clocked_in'
).aggregate(total_duration=Sum('duration'))
hours = 0
# Convert to hours floating point
hours = round((total_duration['total_duration'].total_seconds() / 3600), 2)
if periods_duration_sum['total_duration'] != None:
hours = round((periods_duration_sum['total_duration'].total_seconds() / 3600), 2)
context['period_list'] = Period.objects.filter(
student=self.request.user.student
@ -52,6 +53,7 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
clocked_in__month=timezone.now().month
).order_by('-clocked_in')
context['period_total'] = hours
context['current_period'] = Period.objects.get(pk=self.request.user.student.current_period_id)
return context
class AttendanceUpdateView(LoginRequiredMixin, FormView):
@ -65,14 +67,14 @@ class AttendanceUpdateView(LoginRequiredMixin, FormView):
student = Student.objects.get(student_number=student_number)
if student.is_clocked_in:
student.is_clocked_in=False
period = student.period_set.last()
period = student.period_set.get(pk=student.current_period_id)
period.clocked_out=timezone.now()
period.duration=period.clocked_out-period.clocked_in
student.save()
period.save()
messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked out.')
else:
student.period_set.create(student=student, station_number=station_number)
c_p = student.period_set.create(student=student, clocked_in=timezone.now(), station_number=station_number)
student.current_period_id = c_p.id
student.is_clocked_in=True
student.save()
messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked in.')
@ -87,7 +89,7 @@ class AttendanceUpdateView(LoginRequiredMixin, FormView):
# PERIODS
class PeriodCreateView(LoginRequiredMixin, CreateView):
model = Period
fields = ['student']
form_class = PeriodForm
template_name = 'attendance/period_form.html'
class PeriodDetailView(LoginRequiredMixin, DetailView):
@ -96,9 +98,17 @@ class PeriodDetailView(LoginRequiredMixin, DetailView):
class PeriodUpdateView(LoginRequiredMixin, UpdateView):
model = Period
fields = ['clocked_out']
form_class = PeriodForm
template_name = 'attendance/period_form.html'
# When editing a period, check if it matches the current period of the student and clock them out
def form_valid(self, form):
student = form.instance.student
if form.instance.id == student.current_period_id:
student.is_clocked_in = False
student.save()
return super().form_valid(form)
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse('period-detail', kwargs={'pk': pk})

0
home/__init__.py Normal file
View File

3
home/admin.py Normal file
View File

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

5
home/apps.py Normal file
View File

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

View File

3
home/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,6 @@
{% extends 'application.html' %}
{% load static %}
{% block content %}
<h1>Welcome home</h1>
{% endblock %}

3
home/tests.py Normal file
View File

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

12
home/urls.py Normal file
View File

@ -0,0 +1,12 @@
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.HomeView.as_view(), name='home'),
]

5
home/views.py Normal file
View File

@ -0,0 +1,5 @@
from django.shortcuts import render
from django.views.generic.base import TemplateView
class HomeView(TemplateView):
template_name = "home/home.html"

View File

@ -6,10 +6,8 @@ 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;
font-size: 100%;
}
body {
@ -17,46 +15,36 @@ body {
margin: 0.25in auto;
background-color: #f7f7f7;
color: #323834;
font-family: 'Heebo', sans-serif;
font-family: 'Lato', sans-serif;
font-weight: 400;
line-height: 1.65;
line-height: 1.75;
}
header {
display: flex;
align-items: baseline;
justify-content: space-between;
}
/* Text Elements */
p {
margin-bottom: 1.15rem;
margin-bottom: 1rem;
}
h1, h2, h3, h4, h5 {
/*margin: 2.75rem 0 1.05rem;*/
margin: 0 0 1.05rem;
font-weight: 800;
line-height: 1.15;
margin: 3rem 0 1.38rem;
font-family: 'Lato', sans-serif;
line-height: 1.3;
}
h1 {
margin-top: 0;
font-size: 2.488em;
font-size: 2.488rem;
}
h2 {
font-size: 2.074em;
font-size: 2.074rem;
}
h3 {
font-size: 1.728em;
font-size: 1.728rem;
}
h4 {
font-size: 1.44em;
font-size: 1.44rem;
}
h5 {
font-size: 1.2em;
font-size: 1.2rem;
}
small {
font-size: 0.833em;
font-size: 0.833rem;
}
a {
@ -65,14 +53,23 @@ a {
white-space: nowrap;
}
small {
font-size: 0.8em;
}
hr {
margin: 0;
border: 0.8px solid #bdc3c7;
}
.navigation {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.header {
display: flex;
align-items: top;
justify-content: space-between;
}
blockquote {
text-align: left;
padding: 0 15px;
@ -109,6 +106,7 @@ label {
input[type=text],
input[type=email],
input[type=number],
input[type=password],
select[multiple=multiple],
textarea,
@ -219,4 +217,4 @@ th {
/* Messages */
.messages > .info {color: green;}
.info {color: green;}

View File

@ -9,11 +9,13 @@
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
{% block head %}
{% endblock %}
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}">
</head>
<body>
<header>
<h4>BTech Time Tracker</h4>
<header class="navigation">
<p><strong>BTech Time Tracker</strong></p>
{% if user.is_authenticated %}
<nav>
<a href="{% url 'account-update' user.id %}">{{ user.first_name }} {{ user.last_name }}</a> /

View File

@ -39,6 +39,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'compressor',
'qr_code',
'home.apps.HomeConfig',
'accounts.apps.AccountsConfig',
'attendance.apps.AttendanceConfig',
]

View File

@ -17,6 +17,7 @@ from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('home.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('accounts.urls')),
path('attendance/', include('attendance.urls')),