Compare commits

..

10 Commits

Author SHA1 Message Date
Nathan Chapman
e951f192a2 Merge remote-tracking branch 'origin/master' 2021-02-09 17:46:22 -07:00
Nathan Chapman
4f6092e29c Merge branch 'develop' 2021-02-09 17:46:09 -07:00
Nathan Chapman
f06daeeb46 Remove clock in login from view 2021-02-09 17:45:51 -07:00
Nathan Chapman
5cefdf0480
Merge pull request #1 from nathanjchapman/develop
Merge remote-tracking branch 'origin/develop' into develop
2021-02-09 17:31:31 -07:00
Nathan Chapman
e0270ee81a Merge remote-tracking branch 'origin/develop' into develop 2021-02-09 17:30:20 -07:00
Nathan Chapman
cb406b4d77 Merge branch 'develop' 2021-02-09 17:27:09 -07:00
Nathan Chapman
768fc04533 Update README and add requirements.txt 2021-02-09 17:26:38 -07:00
Nathan Chapman
df447059c3 Update signal handling for Period model and auto-clockin for Student 2021-02-08 21:32:54 -07:00
Nathan Chapman
03642b3adc Merge branch 'develop' 2021-02-08 20:29:05 -07:00
Nathan Chapman
76339dc65a Add signal for period created to auto-clockin Student 2021-02-08 20:28:18 -07:00
19 changed files with 124 additions and 47 deletions

View File

@ -1,5 +1,16 @@
# BTech Time Tracker
### To-Do
### Getting started
Requirements
- install pipenv and run `pipenv install` to install dependencies
- run `pipenv shell` to activate the virtual environment
Django
- `python manage.py makemigrations`
- `python manage.py migrate`
- `python manage.py createsuperuser`
- `python manage.py runserver`
- Add a "current_period" field to student that stores the fk of their current period.

View File

@ -2,6 +2,7 @@ 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)

View File

@ -10,7 +10,7 @@
{% elif user.instructor %}
<p>{{ user.instructor.department }}</p>
{% endif %}
{% if periods %}
{% if periods and periods.total %}
<p>Total clocked hours: <strong>{{ periods.total|timedelta_format }}</strong></p>
{% endif %}
<p>

View File

@ -3,3 +3,6 @@ from django.apps import AppConfig
class AttendanceConfig(AppConfig):
name = 'attendance'
def ready(self):
import attendance.signals

View File

@ -3,6 +3,7 @@ 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)
station_number = models.IntegerField()

24
attendance/signals.py Normal file
View File

@ -0,0 +1,24 @@
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Period
@receiver(post_save, sender=Period)
def student_period_created_updated(sender, instance, created, **kwargs):
if created and not instance.clocked_out:
instance.student.is_clocked_in = True
instance.student.current_period_id = instance.pk
instance.student.save()
elif not created and not instance.clocked_out:
instance.student.is_clocked_in = True
instance.student.current_period_id = instance.pk
instance.student.save()
elif instance.clocked_out and not created:
instance.student.is_clocked_in = False
instance.student.save()
@receiver(post_delete, sender=Period)
def student_period_deleted(sender, instance, **kwargs):
if instance.student.current_period_id == instance.pk:
instance.student.is_clocked_in = False
instance.student.current_period_id = None
instance.student.save()

View File

@ -7,7 +7,7 @@
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="clock-in/out">
<input type="submit" value="Submit">
</form>
</section>
{% endblock %}

View File

@ -93,7 +93,9 @@
{% for student in student_list %}
<tr>
<td>{{ student.user.first_name }} {{ student.user.last_name }}</td>
{% if student.total_hours %}
<td><strong>{{ student.total_hours|timedelta_format }}</strong></td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="2">No periods yet.</td></tr>

View File

@ -20,7 +20,11 @@
</section>
<section class="student__attendance attendance">
<h2 class="attendance__title">Attendance log</h2>
<p class="attendance__total">Total hours for the month: <strong>{{ period_total.total|timedelta_format:2 }}</strong> <small>(Does not include current session.)</small></p>
{% if monthly_total.duration %}
<p class="attendance__total">Total hours for the month: <strong>{{ monthly_total.duration|timedelta_format }}</strong>
<small>(Does not include current session.)</small>
</p>
{% endif %}
{% include 'attendance/_student_periods.html' %}
<p>
<a class="action-button" href="{% url 'period-list' %}">See previous →</a>

View File

@ -2,9 +2,13 @@
{% 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>
<section class="panel">
<h1>Delete Session</h1>
<form method="post">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm"> or
<a href="{% url 'period-detail' object.pk %}">Cancel</a>
</form>
</section>
{% endblock %}

View File

@ -1,26 +1,32 @@
{% extends 'application.html' %}
{% load timedelta_filter %}
{% block content %}
<h1>Period</h1>
<section>
<header>
<section class="period panel">
<h1>Session</h1>
<header class="period__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>
<dl class="period__data">
<dt>Student</dt>
<dd>{{ period.student }}</dd>
<dt>Clocked in</dt>
<dd>{{ period.clocked_in }}</dd>
{% if period.clocked_out %}
<dt>Clocked out</dt>
<dd>{{ period.clocked_out }}</dd>
{% else %}
<dt>Not clocked out.</dt>
{% endif %}
{% if period.duration %}
<dt>Duration</dt>
<dd>{{ period.duration }}</dd>
<dd>{{ period.duration|timedelta_format }} hours</dd>
{% endif %}
</dl>
</section>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block content %}
<section class="period panel">
<h1>Generate Period</h1>
<h1>Session</h1>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}

View File

@ -12,23 +12,16 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.utils import timezone
from django.contrib import messages
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from accounts.models import Instructor, Student
from .models import Code, Period
from .forms import AttendanceUpdateForm, PeriodForm
# EXAMPLE PERMISSION MIXIN
# class MyView(PermissionRequiredMixin, View):
# permission_required = 'polls.add_choice'
# # Or multiple of permissions:
# permission_required = ('polls.view_choice', 'polls.change_choice')
# OVERVIEW
class AttendanceOverview(LoginRequiredMixin, TemplateView):
template_name = 'attendance/attendance_overview.html'
def get_queryset(self):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
@ -50,13 +43,13 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
student = self.request.user.student
# sum all duration fields for student
context['period_total'] = Period.objects.filter(
context['monthly_total'] = Period.objects.filter(
student = student
).filter(
clocked_in__year=timezone.now().year
).filter(
clocked_in__month=timezone.now().month
).aggregate(total=Sum('duration'))
).aggregate(duration=Sum('duration'))
context['period_list'] = Period.objects.filter(
student=student
@ -74,22 +67,16 @@ class AttendanceUpdateView(LoginRequiredMixin, FormView):
form_class = AttendanceUpdateForm
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.get(pk=student.current_period_id)
period.clocked_out=timezone.now()
student.save()
period.save()
messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked out.')
else:
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()
period = student.period_set.create(student=student, clocked_in=timezone.now(), station_number=station_number)
messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked in.')
return super().form_valid(form)
@ -132,14 +119,6 @@ class PeriodUpdateView(LoginRequiredMixin, UpdateView):
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})

View File

@ -5,18 +5,18 @@
<article class="home">
<header class="panel">
<h1>Welcome to<br>
Btech Time Tracker</h1>
BTech Time Tracker</h1>
<h2>Know exactly how many hours you've clocked, anytime, anywhere.</h2>
</header>
<section>
<section class="home__section">
<h3>No more paper</h3>
<p>No more reading bad handwriting or putting the wrong date or time.</p>
</section>
<section>
<section class="home__section">
<h3>No more questions</h3>
<p>No more asking your instructor, or being asked by your student how many hours they have.</p>
</section>
<section>
<section class="home__section">
<h3>No more manual entry</h3>
<p>We invented computers to do that for us.</p>
</section>

27
requirements.txt Normal file
View File

@ -0,0 +1,27 @@
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements
#
-i https://pypi.org/simple
asgiref==3.3.1; python_version >= '3.5'
django-appconf==1.0.4
django-compressor==2.4
django-easy-timezones==0.8.0
django-libsass==0.8
django-qr-code==2.1.0
django==3.1.6
ipaddress==1.0.16
libsass==0.20.1
pillow==8.1.0
pygeoip==0.3.2
pytz==2016.6
qrcode==6.1
rcssmin==1.0.6
rjsmin==1.1.0
segno==1.3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.1; python_version >= '3.5'
wheel==0.29.0

View File

@ -2,4 +2,8 @@
h1, h2 {
text-align: center;
}
&__section {
margin: 0 1rem;
}
}

View File

@ -0,0 +1,6 @@
.period {
&__header {
display: flex;
justify-content: space-between;
}
}

View File

@ -8,6 +8,11 @@ class TimezoneMiddleware:
def __call__(self, request):
tzname = request.session.get('django_timezone')
if request.user.is_authenticated:
if hasattr(request.user, 'instructor'):
tzname = request.user.instructor.timezone
if hasattr(request.user, 'student'):
tzname = request.user.student.timezone
if tzname:
timezone.activate(pytz.timezone(tzname))
else:

View File

@ -149,4 +149,4 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
LOGIN_REDIRECT_URL = '/accounts/timezone/'
LOGIN_REDIRECT_URL = '/attendance/'