Minimum viable product, acheived
This commit is contained in:
parent
c434b973cc
commit
ffd991c066
1
Pipfile
1
Pipfile
@ -8,6 +8,7 @@ django = "*"
|
|||||||
django-compressor = "*"
|
django-compressor = "*"
|
||||||
qrcode = "*"
|
qrcode = "*"
|
||||||
pillow = "*"
|
pillow = "*"
|
||||||
|
django-qr-code = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|||||||
18
Pipfile.lock
generated
18
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "35627f97351787e78b306a9071b0db88411b941484359b8622cd032f3f6a64f2"
|
"sha256": "d1dd018056bda89f00de6e3fbdd88cfa2fc56b756047e88153a4f8e99171e1bb"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -47,6 +47,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.4"
|
"version": "==2.4"
|
||||||
},
|
},
|
||||||
|
"django-qr-code": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5fdd84f4c02127c262e6987bd1a9396a47f2b806be04aa0bebfcb786a9f6a0b7",
|
||||||
|
"sha256:ff495d9628e3d02ad8581c3b3f17940008d6613c69ffdc5abb67e02f8159db6e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.1.0"
|
||||||
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
|
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
|
||||||
@ -124,6 +132,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"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": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
|
|||||||
@ -8,6 +8,9 @@ class Department(models.Model):
|
|||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class Instructor(models.Model):
|
class Instructor(models.Model):
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
department = models.ForeignKey(Department, on_delete=models.CASCADE)
|
department = models.ForeignKey(Department, on_delete=models.CASCADE)
|
||||||
|
|||||||
@ -14,6 +14,6 @@
|
|||||||
|
|
||||||
<input type="submit" value="Save changes" />
|
<input type="submit" value="Save changes" />
|
||||||
</form>
|
</form>
|
||||||
<p><a href="{% url 'password_change' %}">Update password</a></p>
|
<p><a href="{% url 'password_change' %}">Change password</a></p>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,2 +1,9 @@
|
|||||||
from django import forms
|
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})
|
||||||
|
)
|
||||||
|
|||||||
@ -1,23 +1,40 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from accounts.models import Student
|
from accounts.models import Student
|
||||||
|
|
||||||
class Code(models.Model):
|
class Code(models.Model):
|
||||||
student = models.ForeignKey(Student, on_delete=models.CASCADE)
|
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):
|
def __str__(self):
|
||||||
return str(self.name)
|
return f'{self.student.student_number}:{self.station_number}'
|
||||||
|
|
||||||
class Period(models.Model):
|
class Period(models.Model):
|
||||||
student = models.ForeignKey(Student, on_delete=models.CASCADE)
|
student = models.ForeignKey(Student, on_delete=models.CASCADE)
|
||||||
clocked_in = models.DateTimeField(auto_now_add=True)
|
clocked_in = models.DateTimeField(auto_now_add=True)
|
||||||
clocked_out = models.DateTimeField(blank=True, null=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)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def duration(self):
|
# def duration(self):
|
||||||
return round(self.clocked_out - self.clocked_in)
|
# 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):
|
def __str__(self):
|
||||||
return f'{self.clocked_in}: {self.student.user.first_name} {self.student.user.last_name}'
|
return f'{self.clocked_in}: {self.student.user.first_name} {self.student.user.last_name}'
|
||||||
|
|||||||
14
attendance/templates/attendance/attendance_form.html
Normal file
14
attendance/templates/attendance/attendance_form.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Scan code to clock-in/out</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{% url 'attendance-update' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="clock-in/out">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@ -1,29 +1,76 @@
|
|||||||
<section>
|
<section>
|
||||||
<header>
|
<header>
|
||||||
<p>{{ user.instructor.department.name }}</p>
|
<h1>{{ user.instructor.department.name }}</h1>
|
||||||
<a href="">Update profile</a>
|
<p>
|
||||||
|
<a href="">Generate Reports</a>
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
|
|
||||||
|
<h3>Active sessions</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Student</th>
|
||||||
|
<th>Station</th>
|
||||||
|
<th>Clocked in</th>
|
||||||
|
<th colspan="2">Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for period in period_list %}
|
||||||
|
{% if not period.clocked_out %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ period.student }}</td>
|
||||||
|
<td>{{ period.station_number }}</td>
|
||||||
|
<td>{{ period.clocked_in }}</td>
|
||||||
|
{% if period.clocked_out %}
|
||||||
|
<td>{{ period.clocked_out }}</td>
|
||||||
|
<td>{{ period.clocked_in|timesince:period.clocked_out }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>Current sesson: {{ period.clocked_in|timesince }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><a href="{% url 'period-detail' period.id %}">View</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="2">No periods yet.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<article>
|
<article>
|
||||||
<h2>Attendance log</h2>
|
<div>
|
||||||
<table>
|
<h3>Attendance log</h3>
|
||||||
<tbody>
|
<table>
|
||||||
{% for student in students %}
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ student.user.first_name }}</td>
|
<th>Student</th>
|
||||||
{% for period in student.period_set.all %}
|
<th>Station</th>
|
||||||
<td>{{ period.clocked_in }}</td>
|
<th>Clocked in</th>
|
||||||
{% if period.clocked_out %}
|
<th>Clocked out</th>
|
||||||
<td>{{ period.clocked_out }}</td>
|
<th colspan="2">Duration</th>
|
||||||
{% else %}
|
|
||||||
<td>You have not clocked out.</td>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
</thead>
|
||||||
<tr><td colspan="2">No periods yet.</td></tr>
|
<tbody>
|
||||||
{% endfor %}
|
{% for period in period_list %}
|
||||||
</tbody>
|
{% if period.clocked_out %}
|
||||||
</table>
|
<tr>
|
||||||
|
<td>{{ period.student }}</td>
|
||||||
|
<td>{{ period.station_number }}</td>
|
||||||
|
<td>{{ period.clocked_in }}</td>
|
||||||
|
{% if period.clocked_out %}
|
||||||
|
<td>{{ period.clocked_out }}</td>
|
||||||
|
<td>{{ period.clocked_in|timesince:period.clocked_out }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td colspan="2">Current sesson: {{ period.clocked_in|timesince }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><a href="{% url 'period-detail' period.id %}">View</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="2">No periods yet.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -1,30 +1,48 @@
|
|||||||
<section>
|
<section>
|
||||||
<header>
|
<header>
|
||||||
<p>{{ user.student.department.name }}</p>
|
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
|
||||||
<a href="{% url 'account-update' user.id %}">Update profile</a>
|
<a href="{% url 'account-update' user.id %}">Update profile</a>
|
||||||
</header>
|
</header>
|
||||||
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
|
<h5>{{ user.student.department.name }}</h5>
|
||||||
{% if user.student.is_clocked_in %}
|
{% if user.student.is_clocked_in %}
|
||||||
<p>You are currently clocked in.</p>
|
<p>You are currently clocked in.<br>
|
||||||
<a href="">Clock out ></a>
|
Current sesson: {{ period_list.first.clocked_in|timesince }}</p>
|
||||||
|
<a href="{% url 'code-update' user.student.code_set.last.id %}">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>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You are not clocked in.</p>
|
<p>You are not clocked in.</p>
|
||||||
<a href="">Clock in ></a>
|
<a href="{% url 'code-create' %}">Clock in →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
<article>
|
<article>
|
||||||
<h2>Attendance log</h2>
|
<h2>Attendance log</h2>
|
||||||
|
<p>Total hours for the month: {{period_total}}<br>
|
||||||
|
<small>(Does not include current session.)</small></p>
|
||||||
<table>
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Station</th>
|
||||||
|
<th>Clocked in</th>
|
||||||
|
<th>Clocked out</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for period in user.student.period_set.all %}
|
{% for period in period_list %}
|
||||||
<tr>
|
{% if period.clocked_out %}
|
||||||
<td>{{ period.clocked_in }}</td>
|
<tr>
|
||||||
{% if period.clocked_out %}
|
<td>{{ period.station_number }}</td>
|
||||||
<td>{{ period.clocked_out }}</td>
|
<td>{{ period.clocked_in }}</td>
|
||||||
{% else %}
|
{% if period.clocked_out %}
|
||||||
<td>You have not clocked out.</td>
|
<td>{{ period.clocked_out }}</td>
|
||||||
{% endif %}
|
<td>{{ period.clocked_in|timesince:period.clocked_out }}</td>
|
||||||
</tr>
|
{% else %}
|
||||||
|
<td colspan="2">Current sesson: {{ period.clocked_in|timesince }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="2">No periods yet.</td></tr>
|
<tr><td colspan="2">No periods yet.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
14
attendance/templates/attendance/code_detail.html
Normal file
14
attendance/templates/attendance/code_detail.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
{% load qr_code %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Clock in</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
{% qr_from_text code.qr_code_str size=24 version=2 %}
|
||||||
|
</p>
|
||||||
|
<p><em>Scan code at clock-in station</em></p>
|
||||||
|
<a href="{% url 'attendance-overview' %}">Done</a>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
14
attendance/templates/attendance/code_form.html
Normal file
14
attendance/templates/attendance/code_form.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Generate code</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{% url 'code-create' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Generate code" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
14
attendance/templates/attendance/code_update_form.html
Normal file
14
attendance/templates/attendance/code_update_form.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Generate code</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{% url 'code-update' code.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Generate code" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
10
attendance/templates/attendance/period_confirm_delete.html
Normal file
10
attendance/templates/attendance/period_confirm_delete.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Are you sure you want to delete "{{ object }}"?</p>
|
||||||
|
<input type="submit" value="Confirm">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
24
attendance/templates/attendance/period_detail.html
Normal file
24
attendance/templates/attendance/period_detail.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Period</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<a href="{% url 'attendance-overview' %}">← Back</a>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'period-update' period.id %}">Edit</a>
|
||||||
|
<a href="{% url 'period-delete' period.id %}">Delete</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<dl>
|
||||||
|
<dt>Student</dt>
|
||||||
|
<dd>{{ period.student }}</dd>
|
||||||
|
<dt>Clocked in</dt>
|
||||||
|
<dd>{{ period.clocked_in }}</dd>
|
||||||
|
<dt>Clocked out</dt>
|
||||||
|
<dd>{{ period.clocked_out }}</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
14
attendance/templates/attendance/period_form.html
Normal file
14
attendance/templates/attendance/period_form.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'application.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Generate Period</h1>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{% url 'period-update' period.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@ -9,11 +9,15 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.AttendanceOverview.as_view(), name='attendance-overview'),
|
path('', views.AttendanceOverview.as_view(), name='attendance-overview'),
|
||||||
path('new/', views.AttendanceCreateView.as_view(), name='attendance-create'),
|
path('update/', views.AttendanceUpdateView.as_view(), name='attendance-update'),
|
||||||
path('<int:pk>/', include([
|
path('periods/<int:pk>/', include([
|
||||||
path('', views.AttendanceDetailView.as_view(), name='attendance-detail'),
|
path('', views.PeriodDetailView.as_view(), name='period-detail'),
|
||||||
path('update/', views.AttendanceUpdateView.as_view(), name='attendance-update'),
|
path('update/', views.PeriodUpdateView.as_view(), name='period-update'),
|
||||||
path('delete/', views.AttendanceDeleteView.as_view(), name='attendance-delete'),
|
path('delete/', views.PeriodDeleteView.as_view(), name='period-delete'),
|
||||||
])),
|
])),
|
||||||
|
] + [
|
||||||
|
path('codes/new/', views.CodeCreateView.as_view(), name='code-create'),
|
||||||
|
path('codes/<int:pk>/', views.CodeDetailView.as_view(), name='code-detail'),
|
||||||
|
path('codes/<int:pk>/update/', views.CodeUpdateView.as_view(), name='code-update'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from django.shortcuts import render, reverse
|
from django.shortcuts import render, reverse
|
||||||
|
from django.db.models import Avg, Count, Min, Sum
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
|
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.views.generic.list import ListView
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
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 django.contrib.auth.models import User
|
||||||
from accounts.models import Instructor, Student
|
from accounts.models import Instructor, Student
|
||||||
from .models import Period
|
from .models import Code, Period
|
||||||
|
from .forms import AttendanceUpdateForm
|
||||||
|
|
||||||
# EXAMPLE PERMISSION MIXIN
|
# EXAMPLE PERMISSION MIXIN
|
||||||
# class MyView(PermissionRequiredMixin, View):
|
# class MyView(PermissionRequiredMixin, View):
|
||||||
@ -16,6 +20,7 @@ from .models import Period
|
|||||||
# # Or multiple of permissions:
|
# # Or multiple of permissions:
|
||||||
# permission_required = ('polls.view_choice', 'polls.change_choice')
|
# permission_required = ('polls.view_choice', 'polls.change_choice')
|
||||||
|
|
||||||
|
# OVERVIEW
|
||||||
class AttendanceOverview(LoginRequiredMixin, TemplateView):
|
class AttendanceOverview(LoginRequiredMixin, TemplateView):
|
||||||
template_name = 'attendance/attendance_overview.html'
|
template_name = 'attendance/attendance_overview.html'
|
||||||
|
|
||||||
@ -23,28 +28,106 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['user'] = User.objects.get(pk=self.request.user.id)
|
context['user'] = User.objects.get(pk=self.request.user.id)
|
||||||
if hasattr(self.request.user, 'instructor'):
|
if hasattr(self.request.user, 'instructor'):
|
||||||
context['students'] = Student.objects.filter(department=self.request.user.instructor.department)
|
context['student_list'] = Student.objects.filter(department=self.request.user.instructor.department)
|
||||||
context['period_list'] = Period.objects.all()
|
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
|
return context
|
||||||
|
|
||||||
class AttendanceCreateView(LoginRequiredMixin, CreateView):
|
class AttendanceUpdateView(LoginRequiredMixin, FormView):
|
||||||
model = Period
|
|
||||||
fields = ['email', 'password']
|
|
||||||
template_name = 'attendance/attendance_form.html'
|
template_name = 'attendance/attendance_form.html'
|
||||||
|
form_class = AttendanceUpdateForm
|
||||||
|
|
||||||
class AttendanceDetailView(LoginRequiredMixin, DetailView):
|
def form_valid(self, form):
|
||||||
model = Period
|
# update checked in
|
||||||
template_name = 'attendance/attendance_detail.html'
|
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
|
model = Period
|
||||||
fields = ['username', 'email']
|
fields = ['student']
|
||||||
template_name = 'attendance/attendance_form.html'
|
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):
|
def get_success_url(self):
|
||||||
pk = self.kwargs["pk"]
|
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
|
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})
|
||||||
|
|||||||
@ -13,9 +13,9 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
max-width: 8.5in;
|
max-width: 1024px;
|
||||||
margin: 0.25in auto;
|
margin: 0.25in auto;
|
||||||
background-color: white;
|
background-color: #f7f7f7;
|
||||||
color: #323834;
|
color: #323834;
|
||||||
font-family: 'Heebo', sans-serif;
|
font-family: 'Heebo', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -183,8 +183,8 @@ textarea {
|
|||||||
|
|
||||||
section {
|
section {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
background-color: white;
|
||||||
border: 1px solid #bdc3c7;
|
border: 1px solid #bdc3c7;
|
||||||
box-shadow: 0 0 4px #bdc3c7;
|
|
||||||
margin-bottom: 1.3em;
|
margin-bottom: 1.3em;
|
||||||
|
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
@ -213,3 +213,10 @@ th {
|
|||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: #bdc3c7;
|
border-color: #bdc3c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.messages > .info {color: green;}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Tracker</title>
|
<title>BTech Tracker</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
||||||
@ -13,9 +13,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h4>Tracker</h4>
|
<h4>BTech Time Tracker</h4>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<nav>
|
<nav>
|
||||||
|
<a href="{% url 'account-update' user.id %}">{{ user.first_name }} {{ user.last_name }}</a> /
|
||||||
<a class="logout" href="{% url 'logout' %}">Logout</a>
|
<a class="logout" href="{% url 'logout' %}">Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -33,6 +34,13 @@
|
|||||||
<main>
|
<main>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<p {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'compressor',
|
'compressor',
|
||||||
|
'qr_code',
|
||||||
'accounts.apps.AccountsConfig',
|
'accounts.apps.AccountsConfig',
|
||||||
'attendance.apps.AttendanceConfig',
|
'attendance.apps.AttendanceConfig',
|
||||||
]
|
]
|
||||||
@ -134,7 +135,7 @@ STATIC_ROOT = '/var/www/static/'
|
|||||||
# Media file storage
|
# Media file storage
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = [BASE_DIR / 'media']
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/attendance'
|
LOGIN_REDIRECT_URL = '/attendance'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user