Skip to content

Commit 83788ad

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

File tree

7 files changed

+157
-47
lines changed

7 files changed

+157
-47
lines changed

intranet/apps/eighth/migrations/0072_auto_20250429_1128.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ class Migration(migrations.Migration):
2626
migrations.AddField(
2727
model_name='eighthscheduledactivity',
2828
name='attendance_code',
29-
field=models.CharField(default=intranet.apps.eighth.models.EighthScheduledActivity.random_code, max_length=6),
29+
field=models.CharField(default=intranet.apps.eighth.models.random_code, max_length=6),
3030
),
3131
migrations.AddField(
3232
model_name='historicaleighthscheduledactivity',
3333
name='attendance_code',
34-
field=models.CharField(default=intranet.apps.eighth.models.EighthScheduledActivity.random_code, max_length=6),
34+
field=models.CharField(default=intranet.apps.eighth.models.random_code, max_length=6),
3535
),
3636
migrations.RunPython(generate_attendance_codes, reverse_code=migrations.RunPython.noop),
3737
migrations.AddField(

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.choice(string.ascii_uppercase + string.digits) for _ in range(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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
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<act_id>\w+)/(?P<code>\w+)$", attendance.qr_attendance_view, name="qr_attendance"),
2425
# Teachers
2526
re_path(r"^/attendance$", attendance.teacher_choose_scheduled_activity_view, name="eighth_attendance_choose_scheduled_activity"),
2627
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: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def take_attendance_view(request, scheduled_activity_id):
430430
"show_checkboxes": (scheduled_activity.block.locked or request.user.is_eighth_admin),
431431
"show_icons": (scheduled_activity.block.locked and scheduled_activity.block.attendance_locked() and not request.user.is_eighth_admin),
432432
"bbcu_script": settings.BBCU_SCRIPT,
433-
"is_sponsor": scheduled_activity.user_is_sponsor(request.user),
433+
"qrurl": request.build_absolute_uri(reverse("qr_attendance", args=[scheduled_activity.id, scheduled_activity.attendance_code])),
434434
}
435435

436436
if request.user.is_eighth_admin:
@@ -774,31 +774,21 @@ def email_students_view(request, scheduled_activity_id):
774774

775775
@login_required
776776
@deny_restricted
777-
def student_attendance_view(request):
777+
def student_attendance_view(request, attc=None, attf=None, attimef=None, atteachf=None):
778778
blocks = EighthBlock.objects.get_blocks_today()
779-
attc = None
780-
attf = None
781-
attimef = None
782-
atteachf = None
783779
if request.method == "POST":
784780
now = timezone.localtime()
785-
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks.all()
781+
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
786782
for blk in blocks:
787783
blklet = blk.block_letter
788784
code = request.POST.get(blklet)
789785
if code is None:
790786
continue
791787
act = request.user.eighthscheduledactivity_set.get(block=blk)
792788
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:
789+
try:
790+
dayblk = dayblks.get(name="8" + blklet)
791+
except Exception:
802792
attimef = blk
803793
break
804794
start_time = shift_time(tm(hour=dayblk.start.hour, minute=dayblk.start.minute), -20)
@@ -811,34 +801,95 @@ def student_attendance_view(request):
811801
break
812802
code = code.upper()
813803
if code == act.attendance_code:
814-
present = EighthSignup.objects.filter(scheduled_activity=act, user__in=[request.user.id])
815-
present.update(was_absent=False)
816-
attc = blk
817-
for s in present:
818-
invalidate_obj(s)
819-
act.attendance_taken = True
820-
act.save()
821-
invalidate_obj(act)
804+
try:
805+
present = EighthSignup.objects.get(scheduled_activity=act, user__in=[request.user.id])
806+
present.was_absent = False
807+
invalidate_obj(present)
808+
act.attendance_taken = True
809+
act.save()
810+
invalidate_obj(act)
811+
attc = blk
812+
except Exception:
813+
attf = blk
822814
break
823815
else:
824816
attf = blk
825817
break
818+
return student_frontend(request, attc, attf, attimef, atteachf)
819+
820+
821+
@login_required
822+
@deny_restricted
823+
def student_frontend(request, attc=None, attf=None, attimef=None, atteachf=None):
824+
blocks = EighthBlock.objects.get_blocks_today()
826825
if blocks:
827826
sch_acts = []
827+
att_marked = []
828828
for b in blocks:
829829
try:
830830
act = request.user.eighthscheduledactivity_set.get(block=b)
831831
if act.activity.name != "z - Hybrid Sticky":
832832
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-
833+
signup = EighthSignup.objects.get(user=request.user, scheduled_activity=act)
834+
if not signup.was_absent:
835+
att_marked.append(b)
834836
except EighthScheduledActivity.DoesNotExist:
835837
sch_acts.append([b, None])
836838
response = render(
837839
request,
838840
"eighth/student_submit_attendance.html",
839-
context={"sch_acts": sch_acts, "attc": attc, "attf": attf, "attimef": attimef, "atteachf": atteachf},
841+
context={"sch_acts": sch_acts, "att_marked": att_marked, "attc": attc, "attf": attf, "attimef": attimef, "atteachf": atteachf},
840842
)
841843
else:
842844
messages.error(request, "There are no eighth period blocks scheduled today.")
843845
response = redirect("index")
844846
return response
847+
848+
849+
@login_required
850+
@deny_restricted
851+
def qr_attendance_view(request, act_id, code):
852+
act = get_object_or_404(EighthScheduledActivity, id=act_id)
853+
error = False
854+
block = act.block
855+
attc = None
856+
attf = None
857+
attimef = None
858+
atteachf = None
859+
if act.get_code_mode_display() == "Auto":
860+
now = timezone.localtime()
861+
dayblks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
862+
try:
863+
dayblk = dayblks.get(name="8" + block.block_letter)
864+
start_time = shift_time(tm(hour=dayblk.start.hour, minute=dayblk.start.minute), -20)
865+
end_time = shift_time(tm(hour=dayblk.end.hour, minute=dayblk.end.minute), 20)
866+
if not start_time <= now.time() <= end_time:
867+
attimef = block
868+
error = True
869+
except Exception:
870+
attimef = block
871+
error = True
872+
elif act.get_code_mode_display() == "Closed":
873+
atteachf = block
874+
error = True
875+
if not error:
876+
code = code.upper()
877+
if code == act.attendance_code:
878+
try:
879+
present = EighthSignup.objects.get(scheduled_activity=act, user__in=[request.user.id])
880+
present.was_absent = False
881+
invalidate_obj(present)
882+
act.attendance_taken = True
883+
act.save()
884+
invalidate_obj(act)
885+
attc = block
886+
messages.success(request, "Attendance marked.")
887+
except Exception:
888+
attf = block
889+
messages.error(request, "Failed to mark attendance.")
890+
else:
891+
attf = block
892+
messages.error(request, "Failed to mark attendance.")
893+
else:
894+
messages.error(request, "Failed to mark attendance.")
895+
return student_frontend(request, attc, attf, attimef, atteachf)

intranet/static/vendor/qrcode.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<script src="{% static 'js/eighth/attendance.js' %}"></script>
2424
<script src="{% static 'js/eighth/ui_init.js' %}"></script>
2525
<script src="{% static 'js/eighth/user_link.js' %}"></script>
26+
<script src="{% static 'vendor/qrcode.min.js' %}"></script>
2627
{% if wizard %}
2728
<script>
2829
$(function() {
@@ -439,29 +440,81 @@ <h3>Passes</h3>
439440
{% endfor %}
440441
</tbody>
441442
<tfoot class="print-hide">
443+
{% if not no_edit_perm and not edit_perm_cancelled %}
442444
<tr>
443445
<td colspan="5">
444446
<br>
445447
<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>
448+
<p>Share this code with students present at this activity. Alternatively, share the QR code.</p>
449+
<p>They should input this code into the attendance
450+
form on Ion, accessible from the "Eighth" page.</p>
447451
</td>
448452
</tr>
449453
<tr>
450-
<td colspan="5">
451-
<b>Select Mode:</b>
452-
<form id="mode-form" method="post">
453-
{% csrf_token %}
454-
{% for value, label in scheduled_activity.mode_choices %}
455-
<label>
456-
<input type="radio" name="att_code_mode" value="{{ value }}"
457-
{% if scheduled_activity.code_mode == value %}checked{% endif %}>
458-
{{ label }}
459-
</label>
460-
{% endfor %}
461-
</form>
462-
&nbsp;&nbsp;<button type="submit">Save</button>
463-
</td>
454+
<td colspan="5">
455+
<b>Select Mode:</b>
456+
<form id="mode-form" method="post">
457+
{% csrf_token %}
458+
{% for value, label in scheduled_activity.mode_choices %}
459+
<label>
460+
<input type="radio" name="att_code_mode" value="{{ value }}"
461+
{% if scheduled_activity.code_mode == value %}checked{% endif %}>
462+
{{ label }}
463+
</label>
464+
{% endfor %}
465+
</form>
466+
&nbsp;&nbsp;<button type="submit">Save</button>
467+
468+
<div class="activity-card">
469+
<button type="button" onclick="showQRModal('{{ qrurl }}')">
470+
Show QR Code
471+
</button>
472+
</div>
473+
474+
<div id="qrModal" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh;
475+
background:rgba(0,0,0,0.6); align-items:center; justify-content:center; z-index:1000;">
476+
477+
<div style="background:#fff; padding:20px; border-radius:8px; text-align:center; position:relative;">
478+
<div style="display: flex; flex-direction: row; align-items: center; gap: 40px;">
479+
480+
<div style="font-family: 'Fira Code', 'Consolas', monospace; font-size: 190px;">
481+
{{ scheduled_activity.attendance_code }}
482+
</div>
483+
484+
<div id="qrImageContainer" style="max-width:55000px; max-height:550px;"></div>
485+
</div>
486+
487+
<br>
488+
<button type="button" onclick="closeQRModal()">Close</button>
489+
</div>
490+
</div>
491+
492+
<script>
493+
function showQRModal(qrUrl) {
494+
event.preventDefault();
495+
496+
const modal = document.getElementById('qrModal');
497+
const qrContainer = document.getElementById('qrImageContainer');
498+
499+
qrContainer.innerHTML = '';
500+
501+
new QRCode(qrContainer, {
502+
text: qrUrl,
503+
width: 550,
504+
height: 550,
505+
correctLevel: QRCode.CorrectLevel.H
506+
});
507+
508+
modal.style.display = 'flex';
509+
}
510+
511+
function closeQRModal() {
512+
document.getElementById('qrModal').style.display = 'none';
513+
}
514+
</script>
515+
</td>
464516
</tr>
517+
{% endif %}
465518
<tr>
466519
<td colspan="5">
467520
<br>

0 commit comments

Comments
 (0)