diff --git a/accounts/models.py b/accounts/models.py
index 2e2f3f2..6330c67 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -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}'
diff --git a/accounts/templates/accounts/account_list.html b/accounts/templates/accounts/account_list.html
index 0008997..31fe62d 100644
--- a/accounts/templates/accounts/account_list.html
+++ b/accounts/templates/accounts/account_list.html
@@ -9,7 +9,7 @@
| {{ user.username }} |
{{user.first_name}} {{user.last_name}} |
- Update |
+ Update |
{% empty %}
| No users yet. |
diff --git a/accounts/tests.py b/accounts/tests.py
index 7ce503c..84401bb 100644
--- a/accounts/tests.py
+++ b/accounts/tests.py
@@ -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)
\ No newline at end of file
diff --git a/attendance/forms.py b/attendance/forms.py
index 0fd6005..5a3e492 100644
--- a/attendance/forms.py
+++ b/attendance/forms.py
@@ -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):',
+ }
diff --git a/attendance/models.py b/attendance/models.py
index d17d700..34f5e27 100644
--- a/attendance/models.py
+++ b/attendance/models.py
@@ -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)
diff --git a/attendance/templates/attendance/attendance_overview_instructor.html b/attendance/templates/attendance/attendance_overview_instructor.html
index de5d202..1825ac8 100644
--- a/attendance/templates/attendance/attendance_overview_instructor.html
+++ b/attendance/templates/attendance/attendance_overview_instructor.html
@@ -7,6 +7,9 @@
Active sessions
+
+ + Add new session
+
diff --git a/attendance/templates/attendance/attendance_overview_student.html b/attendance/templates/attendance/attendance_overview_student.html
index 7033e47..134623d 100644
--- a/attendance/templates/attendance/attendance_overview_student.html
+++ b/attendance/templates/attendance/attendance_overview_student.html
@@ -1,13 +1,17 @@
{% endblock %}
diff --git a/attendance/templates/attendance/period_detail.html b/attendance/templates/attendance/period_detail.html
index 5d12cbb..c13619b 100644
--- a/attendance/templates/attendance/period_detail.html
+++ b/attendance/templates/attendance/period_detail.html
@@ -19,6 +19,8 @@
{{ period.clocked_in }}
Clocked out
{{ period.clocked_out }}
+ Duration
+ {{ period.duration }}
{% endblock %}
diff --git a/attendance/templates/attendance/period_form.html b/attendance/templates/attendance/period_form.html
index 4cc43c4..cfd202d 100644
--- a/attendance/templates/attendance/period_form.html
+++ b/attendance/templates/attendance/period_form.html
@@ -4,11 +4,12 @@
Generate Period
{% endblock %}
diff --git a/attendance/tests.py b/attendance/tests.py
index 7ce503c..b7e1499 100644
--- a/attendance/tests.py
+++ b/attendance/tests.py
@@ -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.
diff --git a/attendance/urls.py b/attendance/urls.py
index 3fd30f4..3f170ce 100644
--- a/attendance/urls.py
+++ b/attendance/urls.py
@@ -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//', include([
path('', views.PeriodDetailView.as_view(), name='period-detail'),
path('update/', views.PeriodUpdateView.as_view(), name='period-update'),
diff --git a/attendance/views.py b/attendance/views.py
index 22a31da..f1848cd 100644
--- a/attendance/views.py
+++ b/attendance/views.py
@@ -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(
- student=self.request.user.student
+ 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})
diff --git a/home/__init__.py b/home/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/admin.py b/home/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/home/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/home/apps.py b/home/apps.py
new file mode 100644
index 0000000..90dc713
--- /dev/null
+++ b/home/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class HomeConfig(AppConfig):
+ name = 'home'
diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/models.py b/home/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/home/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/home/templates/home/home.html b/home/templates/home/home.html
new file mode 100644
index 0000000..2d8144b
--- /dev/null
+++ b/home/templates/home/home.html
@@ -0,0 +1,6 @@
+{% extends 'application.html' %}
+{% load static %}
+
+{% block content %}
+Welcome home
+{% endblock %}
diff --git a/home/tests.py b/home/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/home/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/home/urls.py b/home/urls.py
new file mode 100644
index 0000000..d66c424
--- /dev/null
+++ b/home/urls.py
@@ -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'),
+]
diff --git a/home/views.py b/home/views.py
new file mode 100644
index 0000000..5202dac
--- /dev/null
+++ b/home/views.py
@@ -0,0 +1,5 @@
+from django.shortcuts import render
+from django.views.generic.base import TemplateView
+
+class HomeView(TemplateView):
+ template_name = "home/home.html"
diff --git a/static/css/base.css b/static/css/base.css
index 5ce7dd8..8342344 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -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;}
diff --git a/templates/application.html b/templates/application.html
index 6edf910..6c4e9ea 100644
--- a/templates/application.html
+++ b/templates/application.html
@@ -9,11 +9,13 @@
{% block head %}
{% endblock %}
+
+
-
- BTech Time Tracker
+
+ BTech Time Tracker
{% if user.is_authenticated %}