Compare commits
10 Commits
b9630092d5
...
e951f192a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e951f192a2 | ||
|
|
4f6092e29c | ||
|
|
f06daeeb46 | ||
|
|
5cefdf0480 | ||
|
|
e0270ee81a | ||
|
|
cb406b4d77 | ||
|
|
768fc04533 | ||
|
|
df447059c3 | ||
|
|
03642b3adc | ||
|
|
76339dc65a |
15
README.md
15
README.md
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
||||
|
||||
class AttendanceConfig(AppConfig):
|
||||
name = 'attendance'
|
||||
|
||||
def ready(self):
|
||||
import attendance.signals
|
||||
|
||||
@ -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
24
attendance/signals.py
Normal 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()
|
||||
@ -7,7 +7,7 @@
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="clock-in/out">
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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
27
requirements.txt
Normal 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
|
||||
@ -2,4 +2,8 @@
|
||||
h1, h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__section {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
.period {
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -149,4 +149,4 @@ MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
|
||||
LOGIN_REDIRECT_URL = '/accounts/timezone/'
|
||||
LOGIN_REDIRECT_URL = '/attendance/'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user