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 # 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.urls import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
class Department(models.Model): class Department(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)

View File

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

View File

@ -3,3 +3,6 @@ from django.apps import AppConfig
class AttendanceConfig(AppConfig): class AttendanceConfig(AppConfig):
name = 'attendance' 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 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)
station_number = models.IntegerField() 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 %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<input type="submit" value="clock-in/out"> <input type="submit" value="Submit">
</form> </form>
</section> </section>
{% endblock %} {% endblock %}

View File

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

View File

@ -20,7 +20,11 @@
</section> </section>
<section class="student__attendance attendance"> <section class="student__attendance attendance">
<h2 class="attendance__title">Attendance log</h2> <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' %} {% include 'attendance/_student_periods.html' %}
<p> <p>
<a class="action-button" href="{% url 'period-list' %}">See previous →</a> <a class="action-button" href="{% url 'period-list' %}">See previous →</a>

View File

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

View File

@ -1,26 +1,32 @@
{% extends 'application.html' %} {% extends 'application.html' %}
{% load timedelta_filter %}
{% block content %} {% block content %}
<h1>Period</h1>
<section> <section class="period panel">
<header> <h1>Session</h1>
<header class="period__header">
<a href="{% url 'attendance-overview' %}">← Back</a> <a href="{% url 'attendance-overview' %}">← Back</a>
<div> <div>
<a href="{% url 'period-update' period.id %}">Edit</a> <a href="{% url 'period-update' period.id %}">Edit</a>
<a href="{% url 'period-delete' period.id %}">Delete</a> <a href="{% url 'period-delete' period.id %}">Delete</a>
</div> </div>
</header> </header>
<dl> <dl class="period__data">
<dt>Student</dt> <dt>Student</dt>
<dd>{{ period.student }}</dd> <dd>{{ period.student }}</dd>
<dt>Clocked in</dt> <dt>Clocked in</dt>
<dd>{{ period.clocked_in }}</dd> <dd>{{ period.clocked_in }}</dd>
{% if period.clocked_out %}
<dt>Clocked out</dt> <dt>Clocked out</dt>
<dd>{{ period.clocked_out }}</dd> <dd>{{ period.clocked_out }}</dd>
{% else %}
<dt>Not clocked out.</dt>
{% endif %}
{% if period.duration %}
<dt>Duration</dt> <dt>Duration</dt>
<dd>{{ period.duration }}</dd> <dd>{{ period.duration|timedelta_format }} hours</dd>
{% endif %}
</dl> </dl>
</section> </section>
{% endblock %} {% endblock %}

View File

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

View File

@ -12,23 +12,16 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from accounts.models import Instructor, Student from accounts.models import Instructor, Student
from .models import Code, Period from .models import Code, Period
from .forms import AttendanceUpdateForm, PeriodForm 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 # OVERVIEW
class AttendanceOverview(LoginRequiredMixin, TemplateView): class AttendanceOverview(LoginRequiredMixin, TemplateView):
template_name = 'attendance/attendance_overview.html' template_name = 'attendance/attendance_overview.html'
def get_queryset(self):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['user'] = self.request.user context['user'] = self.request.user
@ -50,13 +43,13 @@ class AttendanceOverview(LoginRequiredMixin, TemplateView):
student = self.request.user.student student = self.request.user.student
# sum all duration fields for student # sum all duration fields for student
context['period_total'] = Period.objects.filter( context['monthly_total'] = Period.objects.filter(
student = student student = student
).filter( ).filter(
clocked_in__year=timezone.now().year clocked_in__year=timezone.now().year
).filter( ).filter(
clocked_in__month=timezone.now().month clocked_in__month=timezone.now().month
).aggregate(total=Sum('duration')) ).aggregate(duration=Sum('duration'))
context['period_list'] = Period.objects.filter( context['period_list'] = Period.objects.filter(
student=student student=student
@ -74,22 +67,16 @@ class AttendanceUpdateView(LoginRequiredMixin, FormView):
form_class = AttendanceUpdateForm form_class = AttendanceUpdateForm
def form_valid(self, form): def form_valid(self, form):
# update checked in
student_number = form.cleaned_data['qr_string'].split(':')[0] student_number = form.cleaned_data['qr_string'].split(':')[0]
station_number = form.cleaned_data['qr_string'].split(':')[1] station_number = form.cleaned_data['qr_string'].split(':')[1]
student = Student.objects.get(student_number=student_number) student = Student.objects.get(student_number=student_number)
if student.is_clocked_in: if student.is_clocked_in:
student.is_clocked_in=False
period = student.period_set.get(pk=student.current_period_id) period = student.period_set.get(pk=student.current_period_id)
period.clocked_out=timezone.now() period.clocked_out=timezone.now()
student.save()
period.save() period.save()
messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked out.') messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked out.')
else: else:
c_p = student.period_set.create(student=student, clocked_in=timezone.now(), station_number=station_number) period = 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.') messages.add_message(self.request, messages.INFO, f'{student.user.first_name} {student.user.last_name} clocked in.')
return super().form_valid(form) return super().form_valid(form)
@ -132,14 +119,6 @@ class PeriodUpdateView(LoginRequiredMixin, UpdateView):
form_class = PeriodForm form_class = PeriodForm
template_name = 'attendance/period_form.html' 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): def get_success_url(self):
pk = self.kwargs["pk"] pk = self.kwargs["pk"]
return reverse('period-detail', kwargs={'pk': pk}) return reverse('period-detail', kwargs={'pk': pk})

View File

@ -5,18 +5,18 @@
<article class="home"> <article class="home">
<header class="panel"> <header class="panel">
<h1>Welcome to<br> <h1>Welcome to<br>
Btech Time Tracker</h1> BTech Time Tracker</h1>
<h2>Know exactly how many hours you've clocked, anytime, anywhere.</h2> <h2>Know exactly how many hours you've clocked, anytime, anywhere.</h2>
</header> </header>
<section> <section class="home__section">
<h3>No more paper</h3> <h3>No more paper</h3>
<p>No more reading bad handwriting or putting the wrong date or time.</p> <p>No more reading bad handwriting or putting the wrong date or time.</p>
</section> </section>
<section> <section class="home__section">
<h3>No more questions</h3> <h3>No more questions</h3>
<p>No more asking your instructor, or being asked by your student how many hours they have.</p> <p>No more asking your instructor, or being asked by your student how many hours they have.</p>
</section> </section>
<section> <section class="home__section">
<h3>No more manual entry</h3> <h3>No more manual entry</h3>
<p>We invented computers to do that for us.</p> <p>We invented computers to do that for us.</p>
</section> </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 { h1, h2 {
text-align: center; 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): def __call__(self, request):
tzname = request.session.get('django_timezone') 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: if tzname:
timezone.activate(pytz.timezone(tzname)) timezone.activate(pytz.timezone(tzname))
else: else:

View File

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