diff --git a/Pipfile b/Pipfile
index 26c3d3c..b2b100d 100644
--- a/Pipfile
+++ b/Pipfile
@@ -8,6 +8,7 @@ django = "*"
django-compressor = "*"
qrcode = "*"
pillow = "*"
+django-qr-code = "*"
[dev-packages]
diff --git a/Pipfile.lock b/Pipfile.lock
index 7d124a4..5f3847f 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "35627f97351787e78b306a9071b0db88411b941484359b8622cd032f3f6a64f2"
+ "sha256": "d1dd018056bda89f00de6e3fbdd88cfa2fc56b756047e88153a4f8e99171e1bb"
},
"pipfile-spec": 6,
"requires": {
@@ -47,6 +47,14 @@
"index": "pypi",
"version": "==2.4"
},
+ "django-qr-code": {
+ "hashes": [
+ "sha256:5fdd84f4c02127c262e6987bd1a9396a47f2b806be04aa0bebfcb786a9f6a0b7",
+ "sha256:ff495d9628e3d02ad8581c3b3f17940008d6613c69ffdc5abb67e02f8159db6e"
+ ],
+ "index": "pypi",
+ "version": "==2.1.0"
+ },
"pillow": {
"hashes": [
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
@@ -124,6 +132,14 @@
],
"version": "==1.1.0"
},
+ "segno": {
+ "hashes": [
+ "sha256:309281263ba820e49ce44556a27779709b86769b8f2161f94641a3119684dc4e",
+ "sha256:51634ea16d2104e0a8f9309beb785abfdbc1b8c0fffef5d74c80522eec7490d6"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==1.3.1"
+ },
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
diff --git a/accounts/models.py b/accounts/models.py
index d06bcf1..2e2f3f2 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -8,6 +8,9 @@ class Department(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
+ def __str__(self):
+ return self.name
+
class Instructor(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
department = models.ForeignKey(Department, on_delete=models.CASCADE)
diff --git a/accounts/templates/accounts/account_form.html b/accounts/templates/accounts/account_form.html
index 3706fc9..083ffd0 100644
--- a/accounts/templates/accounts/account_form.html
+++ b/accounts/templates/accounts/account_form.html
@@ -14,6 +14,6 @@
-
Update password
+ Change password
{% endblock %}
diff --git a/attendance/forms.py b/attendance/forms.py
index 0afb906..0fd6005 100644
--- a/attendance/forms.py
+++ b/attendance/forms.py
@@ -1,2 +1,9 @@
from django import forms
+from .models import Period
+class AttendanceUpdateForm(forms.Form):
+ qr_string = forms.CharField(
+ label='Scan QR code',
+ max_length=100,
+ widget=forms.TextInput(attrs={'autofocus': True})
+ )
diff --git a/attendance/models.py b/attendance/models.py
index d49997c..d17d700 100644
--- a/attendance/models.py
+++ b/attendance/models.py
@@ -1,23 +1,40 @@
from django.db import models
+from django.urls import reverse
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)
+ station_number = models.IntegerField()
+
+ created = models.DateTimeField(auto_now_add=True)
+ modified = models.DateTimeField(auto_now=True)
+
+ def qr_code_str(self):
+ return f'{self.student.student_number}:{self.station_number}'
+
+ def get_absolute_url(self):
+ return reverse('code-detail', kwargs={'pk': self.pk})
def __str__(self):
- return str(self.name)
+ return f'{self.student.student_number}:{self.station_number}'
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)
+ station_number = models.IntegerField()
+ duration = models.DurationField(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 duration(self):
+ # duration = self.clocked_out - self.clocked_in
+ # duration_in_s = duration.total_seconds()
+ # return divmod(duration_in_s, 3600)[0]
+
+ def get_absolute_url(self):
+ return reverse('period-detail', kwargs={'pk': self.pk})
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_form.html b/attendance/templates/attendance/attendance_form.html
new file mode 100644
index 0000000..d3c38f9
--- /dev/null
+++ b/attendance/templates/attendance/attendance_form.html
@@ -0,0 +1,14 @@
+{% extends 'application.html' %}
+
+{% block content %}
+Scan code to clock-in/out
+
+
+{% endblock %}
diff --git a/attendance/templates/attendance/attendance_overview_instructor.html b/attendance/templates/attendance/attendance_overview_instructor.html
index 42e96e5..de5d202 100644
--- a/attendance/templates/attendance/attendance_overview_instructor.html
+++ b/attendance/templates/attendance/attendance_overview_instructor.html
@@ -1,29 +1,76 @@
- {{ user.first_name }} {{ user.last_name }}
+
+ Active sessions
+
+
+
+ | Student |
+ Station |
+ Clocked in |
+ Duration |
+
+
+
+ {% for period in period_list %}
+ {% if not period.clocked_out %}
+
+ | {{ period.student }} |
+ {{ period.station_number }} |
+ {{ period.clocked_in }} |
+ {% if period.clocked_out %}
+ {{ period.clocked_out }} |
+ {{ period.clocked_in|timesince:period.clocked_out }} |
+ {% else %}
+ Current sesson: {{ period.clocked_in|timesince }} |
+ {% endif %}
+ View |
+
+ {% endif %}
+ {% empty %}
+ | No periods yet. |
+ {% endfor %}
+
+
- Attendance log
-
-
- {% for student in students %}
+
+
Attendance log
+
+
- | {{ student.user.first_name }} |
- {% for period in student.period_set.all %}
- {{ period.clocked_in }} |
- {% if period.clocked_out %}
- {{ period.clocked_out }} |
- {% else %}
- You have not clocked out. |
- {% endif %}
- {% endfor %}
+ Student |
+ Station |
+ Clocked in |
+ Clocked out |
+ Duration |
- {% empty %}
- | No periods yet. |
- {% endfor %}
-
-
+
+
+ {% for period in period_list %}
+ {% if period.clocked_out %}
+
+ | {{ period.student }} |
+ {{ period.station_number }} |
+ {{ period.clocked_in }} |
+ {% if period.clocked_out %}
+ {{ period.clocked_out }} |
+ {{ period.clocked_in|timesince:period.clocked_out }} |
+ {% else %}
+ Current sesson: {{ period.clocked_in|timesince }} |
+ {% endif %}
+ View |
+
+ {% endif %}
+ {% empty %}
+ | No periods yet. |
+ {% endfor %}
+
+
+
diff --git a/attendance/templates/attendance/attendance_overview_student.html b/attendance/templates/attendance/attendance_overview_student.html
index 46e5eaf..7033e47 100644
--- a/attendance/templates/attendance/attendance_overview_student.html
+++ b/attendance/templates/attendance/attendance_overview_student.html
@@ -1,30 +1,48 @@
- {{ user.first_name }} {{ user.last_name }}
+ {{ user.student.department.name }}
{% if user.student.is_clocked_in %}
- You are currently clocked in.
- Clock out >
+ You are currently clocked in.
+ Current sesson: {{ period_list.first.clocked_in|timesince }}
+ Clock out →
+ {% elif user.student.code_set.last %}
+ You are not clocked in.
+ Clock in →
{% else %}
You are not clocked in.
- Clock in >
+ Clock in →
{% endif %}
Attendance log
+ Total hours for the month: {{period_total}}
+ (Does not include current session.)
+
+
+ | Station |
+ Clocked in |
+ Clocked out |
+ Duration |
+
+
- {% for period in user.student.period_set.all %}
-
- | {{ period.clocked_in }} |
- {% if period.clocked_out %}
- {{ period.clocked_out }} |
- {% else %}
- You have not clocked out. |
- {% endif %}
-
+ {% for period in period_list %}
+ {% if period.clocked_out %}
+
+ | {{ period.station_number }} |
+ {{ period.clocked_in }} |
+ {% if period.clocked_out %}
+ {{ period.clocked_out }} |
+ {{ period.clocked_in|timesince:period.clocked_out }} |
+ {% else %}
+ Current sesson: {{ period.clocked_in|timesince }} |
+ {% endif %}
+
+ {% endif %}
{% empty %}
| No periods yet. |
{% endfor %}
diff --git a/attendance/templates/attendance/code_detail.html b/attendance/templates/attendance/code_detail.html
new file mode 100644
index 0000000..56be2e8
--- /dev/null
+++ b/attendance/templates/attendance/code_detail.html
@@ -0,0 +1,14 @@
+{% extends 'application.html' %}
+{% load qr_code %}
+
+{% block content %}
+Clock in
+
+
+
+ {% qr_from_text code.qr_code_str size=24 version=2 %}
+
+ Scan code at clock-in station
+ Done
+
+{% endblock %}
diff --git a/attendance/templates/attendance/code_form.html b/attendance/templates/attendance/code_form.html
new file mode 100644
index 0000000..f6fb5dd
--- /dev/null
+++ b/attendance/templates/attendance/code_form.html
@@ -0,0 +1,14 @@
+{% extends 'application.html' %}
+
+{% block content %}
+Generate code
+
+
+{% endblock %}
diff --git a/attendance/templates/attendance/code_update_form.html b/attendance/templates/attendance/code_update_form.html
new file mode 100644
index 0000000..ff4806f
--- /dev/null
+++ b/attendance/templates/attendance/code_update_form.html
@@ -0,0 +1,14 @@
+{% extends 'application.html' %}
+
+{% block content %}
+Generate code
+
+
+{% endblock %}
diff --git a/attendance/templates/attendance/period_confirm_delete.html b/attendance/templates/attendance/period_confirm_delete.html
new file mode 100644
index 0000000..54ca616
--- /dev/null
+++ b/attendance/templates/attendance/period_confirm_delete.html
@@ -0,0 +1,10 @@
+{% extends 'application.html' %}
+{% load static %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/attendance/templates/attendance/period_detail.html b/attendance/templates/attendance/period_detail.html
new file mode 100644
index 0000000..5d12cbb
--- /dev/null
+++ b/attendance/templates/attendance/period_detail.html
@@ -0,0 +1,24 @@
+{% extends 'application.html' %}
+
+{% block content %}
+Period
+
+
+
+
+ - Student
+ - {{ period.student }}
+ - Clocked in
+ - {{ period.clocked_in }}
+ - Clocked out
+ - {{ period.clocked_out }}
+
+
+{% endblock %}
diff --git a/attendance/templates/attendance/period_form.html b/attendance/templates/attendance/period_form.html
new file mode 100644
index 0000000..4cc43c4
--- /dev/null
+++ b/attendance/templates/attendance/period_form.html
@@ -0,0 +1,14 @@
+{% extends 'application.html' %}
+
+{% block content %}
+Generate Period
+
+
+{% endblock %}
diff --git a/attendance/urls.py b/attendance/urls.py
index 316ce1d..3fd30f4 100644
--- a/attendance/urls.py
+++ b/attendance/urls.py
@@ -9,11 +9,15 @@ from . import views
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'),
+ path('update/', views.AttendanceUpdateView.as_view(), name='attendance-update'),
+ path('periods//', include([
+ path('', views.PeriodDetailView.as_view(), name='period-detail'),
+ path('update/', views.PeriodUpdateView.as_view(), name='period-update'),
+ path('delete/', views.PeriodDeleteView.as_view(), name='period-delete'),
])),
+] + [
+ path('codes/new/', views.CodeCreateView.as_view(), name='code-create'),
+ path('codes//', views.CodeDetailView.as_view(), name='code-detail'),
+ path('codes//update/', views.CodeUpdateView.as_view(), name='code-update'),
]
diff --git a/attendance/views.py b/attendance/views.py
index 3a73765..22a31da 100644
--- a/attendance/views.py
+++ b/attendance/views.py
@@ -1,4 +1,5 @@
from django.shortcuts import render, reverse
+from django.db.models import Avg, Count, Min, Sum
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
@@ -6,9 +7,12 @@ 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.utils import timezone
+from django.contrib import messages
from django.contrib.auth.models import User
from accounts.models import Instructor, Student
-from .models import Period
+from .models import Code, Period
+from .forms import AttendanceUpdateForm
# EXAMPLE PERMISSION MIXIN
# class MyView(PermissionRequiredMixin, View):
@@ -16,6 +20,7 @@ from .models import Period
# # Or multiple of permissions:
# permission_required = ('polls.view_choice', 'polls.change_choice')
+# OVERVIEW
class AttendanceOverview(LoginRequiredMixin, TemplateView):
template_name = 'attendance/attendance_overview.html'
@@ -23,28 +28,106 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
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()
+ context['student_list'] = Student.objects.filter(department=self.request.user.instructor.department)
+ 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
+ ).filter(
+ clocked_in__year=timezone.now().year
+ ).filter(
+ clocked_in__month=timezone.now().month
+ ).order_by('-clocked_in'
+ ).aggregate(total_duration=Sum('duration'))
+
+ # Convert to hours floating point
+ hours = round((total_duration['total_duration'].total_seconds() / 3600), 2)
+
+ context['period_list'] = 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')
+ context['period_total'] = hours
return context
-class AttendanceCreateView(LoginRequiredMixin, CreateView):
- model = Period
- fields = ['email', 'password']
+class AttendanceUpdateView(LoginRequiredMixin, FormView):
template_name = 'attendance/attendance_form.html'
+ form_class = AttendanceUpdateForm
-class AttendanceDetailView(LoginRequiredMixin, DetailView):
- model = Period
- template_name = 'attendance/attendance_detail.html'
+ def form_valid(self, form):
+ # update checked in
+ student_number = form.cleaned_data['qr_string'].split(':')[0]
+ station_number = form.cleaned_data['qr_string'].split(':')[1]
+ student = Student.objects.get(student_number=student_number)
+ if student.is_clocked_in:
+ student.is_clocked_in=False
+ period = student.period_set.last()
+ 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)
+ 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.')
+ return super().form_valid(form)
-class AttendanceUpdateView(LoginRequiredMixin, UpdateView):
+ def get_success_url(self):
+ return reverse('attendance-update')
+
+
+
+
+# PERIODS
+class PeriodCreateView(LoginRequiredMixin, CreateView):
model = Period
- fields = ['username', 'email']
- template_name = 'attendance/attendance_form.html'
+ fields = ['student']
+ template_name = 'attendance/period_form.html'
+
+class PeriodDetailView(LoginRequiredMixin, DetailView):
+ model = Period
+ template_name = 'attendance/period_detail.html'
+
+class PeriodUpdateView(LoginRequiredMixin, UpdateView):
+ model = Period
+ fields = ['clocked_out']
+ template_name = 'attendance/period_form.html'
def get_success_url(self):
pk = self.kwargs["pk"]
- return reverse('attendance-detail', kwargs={'pk': pk})
+ return reverse('period-detail', kwargs={'pk': pk})
-class AttendanceDeleteView(LoginRequiredMixin, DeleteView):
+class PeriodDeleteView(LoginRequiredMixin, DeleteView):
model = Period
- success_url = reverse_lazy('account-overview')
+ success_url = '/attendance'
+
+
+
+# CODES
+class CodeCreateView(LoginRequiredMixin, CreateView):
+ model = Code
+ fields = ['station_number']
+ template_name = 'attendance/code_form.html'
+
+ def form_valid(self, form):
+ form.instance.student = self.request.user.student
+ return super().form_valid(form)
+
+class CodeDetailView(LoginRequiredMixin, DetailView):
+ model = Code
+ template_name = 'attendance/code_detail.html'
+
+class CodeUpdateView(LoginRequiredMixin, UpdateView):
+ model = Code
+ fields = ['station_number']
+ template_name = 'attendance/code_update_form.html'
+
+ def get_success_url(self):
+ pk = self.kwargs["pk"]
+ return reverse('code-detail', kwargs={'pk': pk})
diff --git a/static/css/base.css b/static/css/base.css
index e453550..5ce7dd8 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -13,9 +13,9 @@ html {
}
body {
- max-width: 8.5in;
+ max-width: 1024px;
margin: 0.25in auto;
- background-color: white;
+ background-color: #f7f7f7;
color: #323834;
font-family: 'Heebo', sans-serif;
font-weight: 400;
@@ -183,8 +183,8 @@ textarea {
section {
padding: 1em;
+ background-color: white;
border: 1px solid #bdc3c7;
- box-shadow: 0 0 4px #bdc3c7;
margin-bottom: 1.3em;
-webkit-box-sizing: border-box;
@@ -213,3 +213,10 @@ th {
border-bottom: 1px solid;
border-color: #bdc3c7;
}
+
+
+
+
+
+/* Messages */
+.messages > .info {color: green;}
diff --git a/templates/application.html b/templates/application.html
index c845155..6edf910 100644
--- a/templates/application.html
+++ b/templates/application.html
@@ -2,7 +2,7 @@
- Tracker
+ BTech Tracker
@@ -13,9 +13,10 @@
- Tracker
+ BTech Time Tracker
{% if user.is_authenticated %}
{% else %}
@@ -33,6 +34,13 @@
{% block content %}
{% endblock %}
+ {% if messages %}
+
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}