Skip to content

Commit be20532

Browse files
committed
feat: qr code attendance with teacher pop-up
1 parent cf984ec commit be20532

File tree

5 files changed

+116
-24
lines changed

5 files changed

+116
-24
lines changed

intranet/apps/eighth/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# pylint: disable=too-many-lines; Allow more than 1000 lines
22
import datetime
33
import logging
4-
import random
4+
import secrets
55
import string
66
from collections.abc import Sequence
77
from typing import Collection, Iterable, List, Optional, Union
@@ -795,6 +795,10 @@ def for_sponsor(self, sponsor: EighthSponsor, include_cancelled: bool = False) -
795795
return sched_acts
796796

797797

798+
def random_code():
799+
return "".join(secrets.choices(string.ascii_uppercase + string.digits, k=6))
800+
801+
798802
class EighthScheduledActivity(AbstractBaseEighthModel):
799803
r"""Represents the relationship between an activity and a block in which it has been scheduled.
800804
Attributes:
@@ -851,9 +855,6 @@ class EighthScheduledActivity(AbstractBaseEighthModel):
851855
blank=True,
852856
)
853857

854-
def random_code():
855-
return "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
856-
857858
attendance_code = models.CharField(max_length=6, default=random_code)
858859
mode_choices = [
859860
(0, "Auto"),

intranet/apps/eighth/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
re_path(r"^/absences/(?P<user_id>\d+)$", attendance.eighth_absences_view, name="eighth_absences"),
2222
re_path(r"^/glance$", signup.eighth_location, name="eighth_location"),
2323
re_path(r"^/student_attendance$", attendance.student_attendance_view, name="student_attendance"),
24+
re_path(r"^/qr/(?P<id>\w+)/(?P<code>\w+)$", attendance.qr_attendance_view, name="qr_attendance"),
25+
re_path(r"^/qr_image/(?P<id>\w+)/(?P<code>\w+)$", attendance.qr_image, name="qr_image"),
2426
# Teachers
2527
re_path(r"^/attendance$", attendance.teacher_choose_scheduled_activity_view, name="eighth_attendance_choose_scheduled_activity"),
2628
re_path(r"^/attendance/(?P<scheduled_activity_id>\d+)$", attendance.take_attendance_view, name="eighth_take_attendance"),

intranet/apps/eighth/views/attendance.py

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
import logging
44
from datetime import time as tm
55
from html import escape
6+
from io import BytesIO
67

8+
import qrcode
79
from cacheops import invalidate_obj
810
from django import http
911
from django.conf import settings
1012
from django.contrib import messages
1113
from django.contrib.auth import get_user_model
1214
from django.contrib.auth.decorators import login_required
1315
from django.db.models import Q
14-
from django.http import Http404
16+
from django.http import Http404, HttpResponse
1517
from django.shortcuts import get_object_or_404, redirect, render
1618
from django.urls import reverse
1719
from django.utils import timezone
@@ -430,7 +432,6 @@ def take_attendance_view(request, scheduled_activity_id):
430432
"show_checkboxes": (scheduled_activity.block.locked or request.user.is_eighth_admin),
431433
"show_icons": (scheduled_activity.block.locked and scheduled_activity.block.attendance_locked() and not request.user.is_eighth_admin),
432434
"bbcu_script": settings.BBCU_SCRIPT,
433-
"is_sponsor": scheduled_activity.user_is_sponsor(request.user),
434435
}
435436

436437
if request.user.is_eighth_admin:
@@ -774,31 +775,21 @@ def email_students_view(request, scheduled_activity_id):
774775

775776
@login_required
776777
@deny_restricted
777-
def student_attendance_view(request):
778+
def student_attendance_view(request, attc=None, attf=None, attimef=None, atteachf=None):
778779
blocks = EighthBlock.objects.get_blocks_today()
779-
attc = None
780-
attf = None
781-
attimef = None
782-
atteachf = None
783780
if request.method == "POST":
784781
now = timezone.localtime()
785-
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks.all()
782+
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
786783
for blk in blocks:
787784
blklet = blk.block_letter
788785
code = request.POST.get(blklet)
789786
if code is None:
790787
continue
791788
act = request.user.eighthscheduledactivity_set.get(block=blk)
792789
if act.get_code_mode_display() == "Auto":
793-
dayblk = None
794-
for bk in dayblks:
795-
name = bk.name
796-
if name is None:
797-
continue
798-
if blklet in name and "8" in name:
799-
dayblk = bk
800-
break
801-
if dayblk is None:
790+
try:
791+
dayblk = dayblks.get(name="8" + blklet)
792+
except Exception:
802793
attimef = blk
803794
break
804795
start_time = shift_time(tm(hour=dayblk.start.hour, minute=dayblk.start.minute), -20)
@@ -823,22 +814,85 @@ def student_attendance_view(request):
823814
else:
824815
attf = blk
825816
break
817+
return student_frontend(request, attc, attf, attimef, atteachf)
818+
819+
820+
@login_required
821+
@deny_restricted
822+
def student_frontend(request, attc=None, attf=None, attimef=None, atteachf=None):
823+
blocks = EighthBlock.objects.get_blocks_today()
826824
if blocks:
827825
sch_acts = []
826+
att_marked = []
828827
for b in blocks:
829828
try:
830829
act = request.user.eighthscheduledactivity_set.get(block=b)
831830
if act.activity.name != "z - Hybrid Sticky":
832831
sch_acts.append([b, act, ", ".join([r.name for r in act.get_true_rooms()]), ", ".join([s.name for s in act.get_true_sponsors()])])
833-
832+
signup = EighthSignup.objects.get(user=request.user, scheduled_activity=act)
833+
if not signup.was_absent:
834+
att_marked.append(b)
834835
except EighthScheduledActivity.DoesNotExist:
835836
sch_acts.append([b, None])
836837
response = render(
837838
request,
838839
"eighth/student_submit_attendance.html",
839-
context={"sch_acts": sch_acts, "attc": attc, "attf": attf, "attimef": attimef, "atteachf": atteachf},
840+
context={"sch_acts": sch_acts, "att_marked": att_marked, "attc": attc, "attf": attf, "attimef": attimef, "atteachf": atteachf},
840841
)
841842
else:
842843
messages.error(request, "There are no eighth period blocks scheduled today.")
843844
response = redirect("index")
844845
return response
846+
847+
848+
@login_required
849+
@deny_restricted
850+
def qr_attendance_view(request, act_id, code):
851+
act = get_object_or_404(EighthScheduledActivity, id=act_id)
852+
error = False
853+
block = act.block
854+
attc = None
855+
attf = None
856+
attimef = None
857+
atteachf = None
858+
if act.get_code_mode_display() == "Auto":
859+
now = timezone.localtime()
860+
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
861+
try:
862+
dayblk = dayblks.get(name="8" + block.block_letter)
863+
start_time = shift_time(tm(hour=dayblk.start.hour, minute=dayblk.start.minute), -20)
864+
end_time = shift_time(tm(hour=dayblk.end.hour, minute=dayblk.end.minute), 20)
865+
if not start_time <= now.time() <= end_time:
866+
attimef = block
867+
except Exception:
868+
attimef = block
869+
elif act.get_code_mode_display() == "Closed":
870+
atteachf = block
871+
if not error:
872+
code = code.upper()
873+
if code == act.attendance_code:
874+
present = EighthSignup.objects.filter(scheduled_activity=act, user__in=[request.user.id])
875+
present.update(was_absent=False)
876+
for s in present:
877+
invalidate_obj(s)
878+
act.attendance_taken = True
879+
act.save()
880+
invalidate_obj(act)
881+
attc = block
882+
else:
883+
attf = block
884+
return student_frontend(request, attc, attf, attimef, atteachf)
885+
886+
887+
def qr_image(request, act_id, code):
888+
url = request.build_absolute_uri(reverse("qr_attendance", args=[act_id, code]))
889+
qr = qrcode.QRCode(
890+
version=1,
891+
box_size=14,
892+
border=2,
893+
)
894+
qr.add_data(url)
895+
img = qr.make_image(fill="black", back_color="white")
896+
buffer = BytesIO()
897+
img.save(buffer, format="PNG")
898+
return HttpResponse(buffer.getvalue(), content_type="image/png")

intranet/templates/eighth/student_submit_attendance.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ <h5><strong>Sponsor(s):</strong> {{ s.3 }}</h5>
3737
<p><strong>Comments:</strong> {% if s.1.comments %}{{ s.1.comments }}{% else %}None{% endif %}</p>
3838
<br>
3939
<p>{{ s.1.activity.description }}</p>
40+
{% if s.0 in att_marked%}
41+
<p style="color: green;">Attendance marked.</p>
42+
{% endif %}
4043
<form method="post" action="">
4144
{% csrf_token %}
4245
<p><strong>Attendance Code: </strong></p>

intranet/templates/eighth/take_attendance.html

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,11 +439,14 @@ <h3>Passes</h3>
439439
{% endfor %}
440440
</tbody>
441441
<tfoot class="print-hide">
442+
{% if not no_edit_perm and not edit_perm_cancelled %}
442443
<tr>
443444
<td colspan="5">
444445
<br>
445446
<b>Attendance code: </b><b style="font-family: 'Fira Code', 'Consolas', monospace; font-size: 24px;">{{ scheduled_activity.attendance_code }}</b>
446-
<p>Share this code with students present at this activity.</p>
447+
<p>Share this code with students present at this activity. They should input this code into the attendance
448+
form on Ion, accessible from the "Eighth" page. Alternatively, share the QR code.
449+
</p>
447450
</td>
448451
</tr>
449452
<tr>
@@ -460,8 +463,37 @@ <h3>Passes</h3>
460463
{% endfor %}
461464
</form>
462465
&nbsp;&nbsp;<button type="submit">Save</button>
466+
<div class="activity-card">
467+
<button type="button" onclick="showQRModal('{% url 'qr_image' id=scheduled_activity.id code=scheduled_activity.attendance_code%}')">
468+
Show QR Code
469+
</button>
470+
</div>
471+
<div id="qrModal" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh;
472+
background:rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:1000; display: none;">
473+
<div style="background:#fff; padding:20px; border-radius:8px; text-align:center; position:relative;">
474+
<img id="qrImage" src="" alt="QR Code" style="max-width:90vw; max-height:80vh; margin-top:10px;">
475+
<br><br>
476+
<b style="font-family: 'Fira Code', 'Consolas', monospace; font-size: 140px;">{{ scheduled_activity.attendance_code }}</b>
477+
<br>
478+
<button type="button" onclick="closeQRModal()">Close</button>
479+
</div>
480+
</div>
481+
<script>
482+
function showQRModal(qrUrl) {
483+
event.preventDefault();
484+
const modal = document.getElementById('qrModal');
485+
const img = document.getElementById('qrImage');
486+
img.src = qrUrl;
487+
modal.style.display = 'flex'; // Show modal
488+
}
489+
490+
function closeQRModal() {
491+
document.getElementById('qrModal').style.display = 'none';
492+
}
493+
</script>
463494
</td>
464495
</tr>
496+
{% endif %}
465497
<tr>
466498
<td colspan="5">
467499
<br>

0 commit comments

Comments
 (0)