Skip to content

Commit 8284c85

Browse files
committed
fix: qr code display and mechanics
1 parent 465d0fe commit 8284c85

File tree

18 files changed

+361
-246
lines changed

18 files changed

+361
-246
lines changed

intranet/apps/dashboard/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ def gen_schedule(user, num_blocks: int = 6, surrounding_blocks: Iterable[EighthB
9393
# don't duplicate this info; already caught
9494
current_signup = current_signup.replace(" (Cancelled)", "")
9595

96+
# check if attendance open, if so, will display attendance button
97+
attendance_open = False
98+
if current_sched_act:
99+
from ..eighth.views.attendance import (
100+
check_attendance_open, # avoid circular import, since attendance imports gen_sponsor_schedule from here
101+
)
102+
103+
attendance_open = check_attendance_open(current_sched_act) is None
104+
sch_act_id = None
105+
if attendance_open:
106+
sch_act_id = current_sched_act.id
107+
96108
info = {
97109
"id": b.id,
98110
"block": b,
@@ -107,6 +119,8 @@ def gen_schedule(user, num_blocks: int = 6, surrounding_blocks: Iterable[EighthB
107119
"signup_time": b.signup_time,
108120
"signup_time_future": b.signup_time_future(),
109121
"rooms": rooms,
122+
"attendance_open": attendance_open,
123+
"sch_act_id": sch_act_id,
110124
}
111125
schedule.append(info)
112126

intranet/apps/eighth/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ def accept_pass(self):
18431843
self.was_absent = False
18441844
self.pass_accepted = True
18451845
self.attendance_marked = True
1846-
self.save(update_fields=["was_absent", "pass_accepted"])
1846+
self.save(update_fields=["was_absent", "pass_accepted", "attendance_marked"])
18471847

18481848
def reject_pass(self):
18491849
"""Rejects an eighth period pass for the EighthSignup object."""

intranet/apps/eighth/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from rest_framework.reverse import reverse
1111

1212
from .models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor
13+
from .views.attendance import check_attendance_open
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -139,6 +140,8 @@ def process_scheduled_activity(
139140
"name_with_flags_for_user": name_with_flags_for_user,
140141
"description": activity.description,
141142
"cancelled": scheduled_activity.cancelled,
143+
"attendance_open": check_attendance_open(scheduled_activity) is None,
144+
"attendance_url": reverse("student_attendance", args=[scheduled_activity.id]),
142145
"favorited": activity.id in favorited_activities,
143146
"subscribed_to": activity.id in subscribed_activities,
144147
"subscriptions_enabled": subscriptions_enabled,

intranet/apps/eighth/urls.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
re_path(r"^/absences$", attendance.eighth_absences_view, name="eighth_absences"),
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"),
23-
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"),
23+
re_path(r"^/student_attendance/(?P<sch_act_id>\d+)(?:/(?P<code>[A-Za-z0-9]+))?$", attendance.student_attendance_view, name="student_attendance"),
2524
# Teachers
2625
re_path(r"^/attendance$", attendance.teacher_choose_scheduled_activity_view, name="eighth_attendance_choose_scheduled_activity"),
2726
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: 129 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
from reportlab.lib.units import inch
2424
from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
2525

26+
from ....settings.__init__ import ATTENDANCE_CODE_BUFFER
2627
from ....utils.date import get_date_range_this_year
2728
from ...auth.decorators import attendance_taker_required, deny_restricted, eighth_admin_required
2829
from ...dashboard.views import gen_sponsor_schedule
29-
from ...schedule.models import Day
30+
from ...schedule.models import Block, Day, shift_time
3031
from ...schedule.views import decode_date
3132
from ..forms.admin.activities import ActivitySelectionForm
3233
from ..forms.admin.blocks import BlockSelectionForm
3334
from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor, EighthWaitlist
3435
from ..tasks import email_scheduled_activity_students_task
3536
from ..utils import get_start_date
36-
from .signup import shift_time
3737

3838
logger = logging.getLogger(__name__)
3939

@@ -290,6 +290,11 @@ def take_attendance_view(request, scheduled_activity_id):
290290
scheduled_activity.save()
291291
invalidate_obj(scheduled_activity)
292292

293+
for signup in EighthSignup.objects.filter(scheduled_activity=scheduled_activity):
294+
signup.attendance_marked = False
295+
signup.save()
296+
invalidate_obj(signup)
297+
293298
messages.success(request, f"Attendance bit cleared for {scheduled_activity}")
294299

295300
redirect_url = reverse(url_name, args=[scheduled_activity.id])
@@ -420,6 +425,21 @@ def take_attendance_view(request, scheduled_activity_id):
420425

421426
members.sort(key=lambda m: m["name"])
422427

428+
auto_time_string = ""
429+
try:
430+
day_block = (
431+
Day.objects.select_related("day_type")
432+
.get(date=scheduled_activity.block.date)
433+
.day_type.blocks.get(name="8" + scheduled_activity.block.block_letter)
434+
)
435+
start_time = shift_time(tm(hour=day_block.start.hour, minute=day_block.start.minute), -ATTENDANCE_CODE_BUFFER)
436+
end_time = shift_time(tm(hour=day_block.end.hour, minute=day_block.end.minute), ATTENDANCE_CODE_BUFFER)
437+
auto_time_string = f"({start_time.strftime('%-I:%M')} - {end_time.strftime('%-I:%M %p')})"
438+
except Day.DoesNotExist:
439+
auto_time_string = ""
440+
except Block.DoesNotExist:
441+
auto_time_string = ""
442+
print(auto_time_string)
423443
context = {
424444
"scheduled_activity": scheduled_activity,
425445
"passes": passes,
@@ -430,7 +450,8 @@ def take_attendance_view(request, scheduled_activity_id):
430450
"show_checkboxes": (scheduled_activity.block.locked or request.user.is_eighth_admin),
431451
"show_icons": (scheduled_activity.block.locked and scheduled_activity.block.attendance_locked() and not request.user.is_eighth_admin),
432452
"bbcu_script": settings.BBCU_SCRIPT,
433-
"qr_url": request.build_absolute_uri(reverse("qr_attendance", args=[scheduled_activity.id, scheduled_activity.attendance_code])),
453+
"qr_url": request.build_absolute_uri(reverse("student_attendance", args=[scheduled_activity.id, scheduled_activity.attendance_code])),
454+
"auto_time_string": auto_time_string,
434455
}
435456

436457
if request.user.is_eighth_admin:
@@ -776,143 +797,123 @@ def email_students_view(request, scheduled_activity_id):
776797

777798
@login_required
778799
@deny_restricted
779-
def student_attendance_view(request):
780-
blocks = EighthBlock.objects.get_blocks_today()
781-
mark_block = None
782-
mark_result = None
783-
if request.method == "POST":
784-
now = timezone.localtime()
785-
try:
786-
day_blocks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
787-
except Day.DoesNotExist:
788-
messages.error(request, "Error. Attendance is only available on school days.")
789-
return redirect("index")
790-
for block in blocks:
791-
block_letter = block.block_letter
792-
code = request.POST.get(block_letter)
793-
if code is None:
794-
continue
795-
act = request.user.eighthscheduledactivity_set.get(block=block)
796-
if act.get_code_mode_display() == "Auto":
797-
try:
798-
day_block = day_blocks.get(name="8" + block_letter)
799-
except Exception:
800-
mark_result = "invalid_time"
801-
mark_block = block
802-
break
803-
start_time = shift_time(tm(hour=day_block.start.hour, minute=day_block.start.minute), -20)
804-
end_time = shift_time(tm(hour=day_block.end.hour, minute=day_block.end.minute), 20)
805-
if not start_time <= now.time() <= end_time:
806-
mark_result = "invalid_time"
807-
mark_block = block
808-
break
809-
elif act.get_code_mode_display() == "Closed":
810-
mark_result = "code_closed"
811-
mark_block = block
812-
break
813-
code = code.upper()
814-
if code == act.attendance_code:
815-
try:
816-
present = EighthSignup.objects.get(scheduled_activity=act, user__in=[request.user.id])
817-
present.was_absent = False
818-
present.attendance_marked = True
819-
present.save()
820-
invalidate_obj(present)
821-
act.attendance_taken = True
822-
act.save()
823-
invalidate_obj(act)
824-
mark_result = "code_correct"
825-
mark_block = block
826-
except Exception:
827-
mark_result = "code_fail"
828-
mark_block = block
829-
break
830-
else:
831-
mark_result = "code_fail"
832-
mark_block = block
833-
break
834-
return student_frontend(request, mark_block, mark_result)
835-
800+
def student_attendance_view(request, sch_act_id, code=None):
801+
"""Handles the initial steps for code or QR code student-based attendance
802+
Args:
803+
request: the user's request
804+
sch_act_id: the id of the EighthScheduledActivity for which the user attempts to take attendance. It is from url, required.
805+
code: the code which the user is attempting to take attendance. From url, optional (if the user just wants to access the page)
806+
Returns:
807+
An HttpResponse rendering the student attendance page for the specified EighthScheduledActivity (if it exists).
808+
Otherwise, redirects to index / dashboard.
809+
"""
810+
result = None # could end as None (no attendance attempt), no_signup, no_school, invalid_time, code_closed, code_fail, or marked
811+
try:
812+
sch_act = EighthScheduledActivity.objects.get(id=sch_act_id)
813+
except EighthScheduledActivity.DoesNotExist:
814+
messages.error(request, "Error marking attendance.")
815+
return redirect("index")
836816

837-
@login_required
838-
@deny_restricted
839-
def student_frontend(request, mark_block: EighthBlock = None, mark_result: str = None):
840-
blocks = EighthBlock.objects.get_blocks_today()
841-
if blocks:
842-
sch_acts = []
843-
att_marked = []
844-
for block in blocks:
845-
try:
846-
act = request.user.eighthscheduledactivity_set.get(block=block)
847-
if act.activity.name != "z - Hybrid Sticky":
848-
sch_acts.append(
849-
[block, act, ", ".join([r.name for r in act.get_true_rooms()]), ", ".join([s.name for s in act.get_true_sponsors()])]
850-
)
851-
signup = EighthSignup.objects.get(user=request.user, scheduled_activity=act)
852-
if (not signup.was_absent) and signup.attendance_marked and act.attendance_taken:
853-
att_marked.append(block)
854-
except EighthScheduledActivity.DoesNotExist:
855-
sch_acts.append([block, None])
856-
results = {
857-
"code_correct": "",
858-
"code_closed": '<p style="color: red;">Error. Ask your teacher to open the attendance code.</p>',
859-
"code_fail": '<p style="color: red;">Invalid Code.</p>',
860-
"invalid_time": f'<p style="color: red;">Invalid time. Please fill this out during {block.block_letter} block.</p>',
861-
}
862-
response = render(
863-
request,
864-
"eighth/student_submit_attendance.html",
865-
context={"sch_acts": sch_acts, "att_marked": att_marked, "mark_block": mark_block, "attendance_result": results.get(mark_result)},
866-
)
817+
try:
818+
user_signup = EighthSignup.objects.get(user=request.user, scheduled_activity=sch_act)
819+
if user_signup.attendance_marked and not user_signup.was_absent:
820+
return student_frontend(request, sch_act_id, result="marked")
821+
except EighthSignup.DoesNotExist:
822+
result = "no_signup"
823+
return student_frontend(request, sch_act_id, result)
824+
825+
is_open = check_attendance_open(sch_act) # returns None if attendance is open, error string if closed
826+
if is_open is None:
827+
if code is not None:
828+
result = mark_attendance(request, sch_act, code)
867829
else:
868-
messages.error(request, "There are no eighth period blocks scheduled today.")
869-
response = redirect("index")
870-
return response
830+
result = is_open
831+
832+
return student_frontend(request, sch_act_id, result)
871833

872834

873835
@login_required
874836
@deny_restricted
875-
def qr_attendance_view(request, act_id, code):
876-
act = get_object_or_404(EighthScheduledActivity, id=act_id)
877-
error = False
837+
def student_frontend(request, sch_act_id, result):
838+
"""Handles the process of rendering the frontend UI for student-based attendance.
839+
It is only called by the student_attendance_view function
840+
Args:
841+
request: the user's request
842+
sch_act_id: the EighthScheduledActivity for which the user attempts to take attendance
843+
result: The result of the student's attempt to take attendance (if there was an attempt)
844+
Returns:
845+
An HttpResponse which renders the UI with a corresponding message. Returns this to student_attendance_view.
846+
"""
847+
848+
result_to_html = {
849+
None: "",
850+
"no_signup": '<p style="color: red;">Error: You are not signed up for this activity.</p>',
851+
"no_school": '<p style="color: red;">Error: Attendance is only available on school days.</p>',
852+
"invalid_time": '<p style="color: red;">Error: Please fill out attendance during the timeframe of this activity.</p>',
853+
"code_closed": '<p style="color: red;">Error: Your activity sponsor has closed the attendance code.</p>',
854+
"code_fail": '<p style="color: red;">Error: Invalid Code.</p>',
855+
"marked": '<p style="color: green;">Attendance marked.</p>',
856+
}
857+
html_text = result_to_html[result]
858+
sch_act = EighthScheduledActivity.objects.get(id=sch_act_id)
859+
date_string = sch_act.block.date.strftime("%b %d")
860+
show_form = result in (None, "code_fail")
861+
return render(
862+
request,
863+
"eighth/student_submit_attendance.html",
864+
context={"result": result, "result_html": html_text, "show_form": show_form, "sch_act": sch_act, "date_string": date_string},
865+
)
866+
867+
868+
def mark_attendance(request, act, code):
869+
"""Handles the process of checking the code and marking attendance for the user.
870+
Args:
871+
request: the user's request
872+
act: the EighthScheduledActivity for which the user attempts to take attendance
873+
code: the code which the user is attempting to take attendance
874+
Returns:
875+
A string reporting the result of the attempt. Either "code_correct" or "code_fail"
876+
"""
877+
if code.upper() == act.attendance_code:
878+
try:
879+
present = EighthSignup.objects.get(scheduled_activity=act, user__in=[request.user.id])
880+
present.was_absent = False
881+
present.attendance_marked = True
882+
present.save()
883+
invalidate_obj(present)
884+
act.attendance_taken = True
885+
act.save()
886+
invalidate_obj(act)
887+
return "marked"
888+
except EighthSignup.DoesNotExist:
889+
return "code_fail"
890+
return "code_fail"
891+
892+
893+
def check_attendance_open(act: EighthScheduledActivity):
894+
"""Checks whether attendance is open for an EighthScheduledActivity.
895+
Args:
896+
act: the EighthScheduledActivity in question
897+
day_blocks: the set of Block objects in the current day's day_type (defined in student_attendance_view)
898+
Returns:
899+
mark_result: a string, either "no_school", "invalid_time" or "code_closed", or None if attendance is open.
900+
"""
878901
block = act.block
879902
mark_result = None
880903
if act.get_code_mode_display() == "Auto":
881904
now = timezone.localtime()
882-
day_blocks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
905+
try:
906+
day_blocks = Day.objects.select_related("day_type").get(date=now).day_type.blocks
907+
except Day.DoesNotExist:
908+
return "no_school"
883909
try:
884910
day_block = day_blocks.get(name="8" + block.block_letter)
885-
start_time = shift_time(tm(hour=day_block.start.hour, minute=day_block.start.minute), -20)
886-
end_time = shift_time(tm(hour=day_block.end.hour, minute=day_block.end.minute), 20)
887-
if not start_time <= now.time() <= end_time:
888-
mark_result = "invalid_time"
889-
error = True
890-
except Exception:
911+
except Block.DoesNotExist:
912+
mark_result = "invalid_time"
913+
start_time = shift_time(tm(hour=day_block.start.hour, minute=day_block.start.minute), -ATTENDANCE_CODE_BUFFER)
914+
end_time = shift_time(tm(hour=day_block.end.hour, minute=day_block.end.minute), ATTENDANCE_CODE_BUFFER)
915+
if not start_time <= timezone.localtime().time() <= end_time:
891916
mark_result = "invalid_time"
892-
error = True
893917
elif act.get_code_mode_display() == "Closed":
894918
mark_result = "code_closed"
895-
error = True
896-
if not error:
897-
code = code.upper()
898-
if code == act.attendance_code:
899-
try:
900-
present = EighthSignup.objects.get(scheduled_activity=act, user__in=[request.user.id])
901-
present.was_absent = False
902-
present.attendance_marked = True
903-
present.save()
904-
invalidate_obj(present)
905-
act.attendance_taken = True
906-
act.save()
907-
invalidate_obj(act)
908-
mark_result = "code_correct"
909-
messages.success(request, "Attendance marked.")
910-
except Exception:
911-
mark_result = "code_fail"
912-
messages.error(request, "Failed to mark attendance.")
913-
else:
914-
mark_result = "code_fail"
915-
messages.error(request, "Failed to mark attendance.")
916-
else:
917-
messages.error(request, "Failed to mark attendance.")
918-
return student_frontend(request, block, mark_result)
919+
return mark_result

0 commit comments

Comments
 (0)