import csv
import io
import logging
from html import escape
from cacheops import invalidate_obj
from formtools.wizard.views import SessionWizardView
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from django import http
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from ....utils.date import get_date_range_this_year
from ...auth.decorators import attendance_taker_required, deny_restricted, eighth_admin_required
from ...dashboard.views import gen_sponsor_schedule
from ...schedule.views import decode_date
from ..forms.admin.activities import ActivitySelectionForm
from ..forms.admin.blocks import BlockSelectionForm
from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor, EighthWaitlist
from ..tasks import email_scheduled_activity_students_task
from ..utils import get_start_date
logger = logging.getLogger(__name__)
[docs]def should_show_activity_list(wizard):
if "default_activity" in wizard.request.GET:
act_id = wizard.request.GET["default_activity"]
default_activity = EighthActivity.objects.filter(id=act_id)
if default_activity.count() == 1:
wizard.default_activity = default_activity[0]
return False
if wizard.request.user.is_eighth_admin:
return True
cleaned_data = wizard.get_cleaned_data_for_step("block") or {}
if cleaned_data:
activities = wizard.get_form("activity").fields["activity"].queryset
if activities.count() == 1:
wizard.default_activity = activities[0]
return False
if activities.count() == 0:
wizard.no_activities = True
return False
return True
[docs]class EighthAttendanceSelectScheduledActivityWizard(SessionWizardView):
FORMS = [("block", BlockSelectionForm), ("activity", ActivitySelectionForm)]
TEMPLATES = {"block": "eighth/take_attendance.html", "activity": "eighth/take_attendance.html"}
[docs] def get_template_names(self):
return [self.TEMPLATES[self.steps.current]]
[docs] def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
context.update({"admin_page_title": "Take Attendance"})
#######
if settings.ENABLE_HYBRID_EIGHTH:
context.update({"hybrid": True})
#######
block = self.get_cleaned_data_for_step("block")
context.update({"show_all_blocks": ("show_all_blocks" in self.request.GET or "block" in self.request.GET)})
context.update({"default_activity_not_scheduled": ("default_activity" in self.request.GET and not block)})
if block:
block = block["block"]
try:
sponsor = self.request.user.eighthsponsor
except (EighthSponsor.DoesNotExist, AttributeError):
sponsor = None
if sponsor and not self.request.user.is_eighthoffice:
context.update({"sponsor_block": block})
sponsoring_filter = Q(sponsors=sponsor) | (Q(sponsors=None) & Q(activity__sponsors=sponsor))
sponsored_activities = EighthScheduledActivity.objects.filter(block=block).filter(sponsoring_filter).order_by("activity__name")
context.update({"sponsored_activities": sponsored_activities})
elif "block" in self.request.GET:
block_id = self.request.GET["block"]
context["redirect_block_id"] = block_id
return context
[docs] def done(self, form_list, **kwargs): # pylint: disable=unused-argument
form_list = list(form_list)
if hasattr(self, "no_activities"):
response = redirect("eighth_attendance_choose_scheduled_activity")
response["Location"] += "?na=1"
return response
if hasattr(self, "default_activity"):
activity = self.default_activity # pylint: disable=no-member; We just checked if the attribute exists
else:
activity = form_list[1].cleaned_data["activity"]
block = form_list[0].cleaned_data["block"]
try:
scheduled_activity = EighthScheduledActivity.objects.get(block=block, activity=activity)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404("The scheduled activity with block {} and activity {} does not exist.".format(block, activity)) from e
if "admin" in self.request.path:
url_name = "eighth_admin_take_attendance"
else:
url_name = "eighth_take_attendance"
return redirect(url_name, scheduled_activity_id=scheduled_activity.id)
_unsafe_choose_scheduled_activity_view = EighthAttendanceSelectScheduledActivityWizard.as_view(
EighthAttendanceSelectScheduledActivityWizard.FORMS, condition_dict={"activity": should_show_activity_list}
)
teacher_choose_scheduled_activity_view = attendance_taker_required(_unsafe_choose_scheduled_activity_view)
admin_choose_scheduled_activity_view = eighth_admin_required(_unsafe_choose_scheduled_activity_view)
@login_required
@deny_restricted
def roster_view(request, scheduled_activity_id):
try:
scheduled_activity = EighthScheduledActivity.objects.get(id=scheduled_activity_id)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404 from e
signups = EighthSignup.objects.filter(scheduled_activity=scheduled_activity)
viewable_members = scheduled_activity.get_viewable_members(request.user)
num_hidden_members = len(scheduled_activity.get_hidden_members(request.user))
is_sponsor = scheduled_activity.user_is_sponsor(request.user)
context = {
"scheduled_activity": scheduled_activity,
"viewable_members": viewable_members,
"num_hidden_members": num_hidden_members,
"signups": signups,
"is_sponsor": is_sponsor,
}
return render(request, "eighth/roster.html", context)
[docs]@login_required
def raw_roster_view(request, scheduled_activity_id):
try:
scheduled_activity = EighthScheduledActivity.objects.get(id=scheduled_activity_id)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404 from e
signups = EighthSignup.objects.filter(scheduled_activity=scheduled_activity)
viewable_members = scheduled_activity.get_viewable_members(request.user)
num_hidden_members = scheduled_activity.get_hidden_members(request.user).count()
context = {
"scheduled_activity": scheduled_activity,
"viewable_members": viewable_members,
"num_hidden_members": num_hidden_members,
"signups": signups,
}
return render(request, "eighth/roster_list.html", context)
@eighth_admin_required
@deny_restricted
def raw_waitlist_view(request, scheduled_activity_id):
try:
scheduled_activity = EighthScheduledActivity.objects.get(id=scheduled_activity_id)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404 from e
context = {"ordered_waitlist": EighthWaitlist.objects.filter(scheduled_activity_id=scheduled_activity.id).order_by("time")}
return render(request, "eighth/waitlist_list.html", context)
[docs]@attendance_taker_required
def take_attendance_view(request, scheduled_activity_id):
try:
scheduled_activity = EighthScheduledActivity.objects.select_related("activity", "block").get(
activity__deleted=False, id=scheduled_activity_id
)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404 from e
# Attendance-only users can only see their own roster
if request.user.is_restricted and not scheduled_activity.user_is_sponsor(request.user):
raise http.Http404
edit_perm = request.user.is_eighth_admin or scheduled_activity.user_is_sponsor(request.user)
edit_perm_cancelled = False
if scheduled_activity.cancelled and not request.user.is_eighth_admin:
edit_perm = False
edit_perm_cancelled = True
if request.method == "POST":
if not edit_perm:
if edit_perm_cancelled:
return render(
request,
"error/403.html",
{"reason": "You do not have permission to take attendance for this activity. The activity was cancelled."},
status=403,
)
else:
return render(
request,
"error/403.html",
{"reason": "You do not have permission to take attendance for this activity. You are not a sponsor."},
status=403,
)
if "admin" in request.path:
url_name = "eighth_admin_take_attendance"
else:
url_name = "eighth_take_attendance"
if "clear_attendance_bit" in request.POST:
scheduled_activity.attendance_taken = False
scheduled_activity.save()
invalidate_obj(scheduled_activity)
messages.success(request, "Attendance bit cleared for {}".format(scheduled_activity))
redirect_url = reverse(url_name, args=[scheduled_activity.id])
if "no_attendance" in request.GET:
redirect_url += "?no_attendance={}".format(request.GET["no_attendance"])
return redirect(redirect_url)
if not scheduled_activity.block.locked and not request.user.is_eighth_admin:
return render(
request,
"error/403.html",
{"reason": "You do not have permission to take attendance for this activity. The block has not been locked yet."},
status=403,
)
if not scheduled_activity.block.locked and request.user.is_eighth_admin:
messages.success(request, "Note: Taking attendance on an unlocked block.")
present_user_ids = list(request.POST.keys())
if request.FILES.get("attendance"):
try:
csv_file = request.FILES["attendance"].read().decode("utf-8")
data = csv.DictReader(io.StringIO(csv_file))
unfound_users = []
for u in data:
try:
user = get_user_model().objects.filter(student_id=u["Email"].split("@")[0].strip())
if len(user) != 1:
unfound_users.append(u["Name"])
else:
present_user_ids.append(user[0])
except KeyError:
messages.info(request, "Error on line containing {}. If this line isn't blank, please contact an admin.".format(u))
if unfound_users:
messages.success(
request,
"Took attendance from file, but could not find signups for {} who were present according to the Google Meet file.".format(
", ".join(unfound_users)
),
)
except (csv.Error, ValueError, KeyError, IndexError):
messages.error(request, "Could not interpret file. Did you upload a Google Meet attendance report without modification?")
csrf = "csrfmiddlewaretoken"
if csrf in present_user_ids:
present_user_ids.remove(csrf)
if "attendance" in present_user_ids:
present_user_ids.remove("attendance")
absent_signups = EighthSignup.objects.filter(scheduled_activity=scheduled_activity).exclude(user__in=present_user_ids)
absent_signups.update(was_absent=True)
for s in absent_signups:
invalidate_obj(s)
present_signups = EighthSignup.objects.filter(scheduled_activity=scheduled_activity, user__in=present_user_ids)
present_signups.update(was_absent=False)
for s in present_signups:
invalidate_obj(s)
passes = EighthSignup.objects.filter(scheduled_activity=scheduled_activity, after_deadline=True, pass_accepted=False)
passes.update(was_absent=True)
for s in passes:
invalidate_obj(s)
scheduled_activity.attendance_taken = True
scheduled_activity.save()
invalidate_obj(scheduled_activity)
messages.success(request, "Attendance updated.")
redirect_url = reverse(url_name, args=[scheduled_activity.id])
if "no_attendance" in request.GET:
redirect_url += "?no_attendance={}".format(request.GET["no_attendance"])
return redirect(redirect_url)
else:
passes = EighthSignup.objects.select_related("user").filter(scheduled_activity=scheduled_activity, after_deadline=True, pass_accepted=False)
users = scheduled_activity.members.exclude(eighthsignup__in=passes)
members = []
absent_user_ids = (
EighthSignup.objects.select_related("user")
.filter(scheduled_activity=scheduled_activity, was_absent=True)
.values_list("user__id", flat=True)
)
pass_users = (
EighthSignup.objects.select_related("user")
.filter(scheduled_activity=scheduled_activity, after_deadline=True, pass_accepted=True)
.values_list("user__id", flat=True)
)
for user in users:
members.append(
{
"id": user.id,
"student_id": user.student_id,
"name": user.last_first, # includes nickname
"grade": user.grade.number if user.grade else None,
"present": (scheduled_activity.attendance_taken and (user.id not in absent_user_ids)),
"had_pass": user.id in pass_users,
"pass_present": (not scheduled_activity.attendance_taken and user.id in pass_users and user.id not in absent_user_ids),
"email": user.tj_email,
}
)
invalidate_obj(user)
members.sort(key=lambda m: m["name"])
context = {
"scheduled_activity": scheduled_activity,
"passes": passes,
"members": members,
"p": pass_users,
"no_edit_perm": not edit_perm,
"edit_perm_cancelled": edit_perm_cancelled,
"show_checkboxes": (scheduled_activity.block.locked or request.user.is_eighth_admin),
"show_icons": (scheduled_activity.block.locked and scheduled_activity.block.attendance_locked() and not request.user.is_eighth_admin),
"bbcu_script": settings.BBCU_SCRIPT,
}
if request.user.is_eighth_admin:
context["scheduled_activities"] = EighthScheduledActivity.objects.filter(block__id=scheduled_activity.block.id)
context["blocks"] = EighthBlock.objects.filter(date__gte=get_start_date(request)).order_by("date", "block_letter")
if request.resolver_match.url_name == "eighth_admin_export_attendance_csv":
response = http.HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="attendance.csv"'
writer = csv.writer(response)
writer.writerow(
[
"Block",
"Activity",
"Name",
"FCPS ID",
"Student ID",
"Grade",
"Email",
"Locked",
"Rooms",
"Sponsors",
"Attendance Taken",
"Present",
"Had Pass",
]
)
for member in members:
row = []
row.append(str(scheduled_activity.block))
row.append(str(scheduled_activity.activity))
row.append(member["name"])
row.append(member["student_id"])
row.append(member["id"])
row.append(member["grade"])
row.append(member["email"])
row.append(scheduled_activity.block.locked)
rooms = scheduled_activity.get_true_rooms()
row.append(", ".join(["{} ({})".format(room.name, room.capacity) for room in rooms]))
sponsors = scheduled_activity.get_true_sponsors()
row.append(" ,".join([sponsor.name for sponsor in sponsors]))
row.append(scheduled_activity.attendance_taken)
row.append(member["present"] if scheduled_activity.block.locked else "N/A")
row.append(member["had_pass"] if scheduled_activity.block.locked else "N/A")
writer.writerow(row)
return response
else:
return render(request, "eighth/take_attendance.html", context)
[docs]@attendance_taker_required
def accept_pass_view(request, signup_id):
if request.method != "POST":
return http.HttpResponseNotAllowed(["POST"], "HTTP 405: METHOD NOT ALLOWED")
try:
signup = EighthSignup.objects.get(id=signup_id)
except EighthSignup.DoesNotExist as e:
raise http.Http404 from e
sponsor = request.user.get_eighth_sponsor()
can_accept = signup.scheduled_activity.block.locked and (
sponsor and (sponsor in signup.scheduled_activity.get_true_sponsors()) or request.user.is_eighth_admin
)
if not can_accept:
return render(request, "error/403.html", {"reason": "You do not have permission to accept this pass."}, status=403)
status = request.POST.get("status")
if status == "accept":
signup.accept_pass()
elif status == "reject":
signup.reject_pass()
signup.save()
if "admin" in request.path:
url_name = "eighth_admin_take_attendance"
else:
url_name = "eighth_take_attendance"
return redirect(url_name, scheduled_activity_id=signup.scheduled_activity.id)
[docs]@attendance_taker_required
def accept_all_passes_view(request, scheduled_activity_id):
if request.method != "POST":
return http.HttpResponseNotAllowed(["POST"], "HTTP 405: METHOD NOT ALLOWED")
try:
scheduled_activity = EighthScheduledActivity.objects.get(id=scheduled_activity_id)
except EighthScheduledActivity.DoesNotExist as e:
raise http.Http404 from e
sponsor = request.user.get_eighth_sponsor()
can_accept = scheduled_activity.block.locked and (sponsor and (sponsor in scheduled_activity.get_true_sponsors()) or request.user.is_eighth_admin)
if not can_accept:
return render(request, "error/403.html", {"reason": "You do not have permission to take accept these passes."}, status=403)
EighthSignup.objects.filter(after_deadline=True, scheduled_activity=scheduled_activity).update(pass_accepted=True, was_absent=False)
invalidate_obj(scheduled_activity)
if "admin" in request.path:
url_name = "eighth_admin_take_attendance"
else:
url_name = "eighth_take_attendance"
return redirect(url_name, scheduled_activity_id=scheduled_activity.id)
[docs]def generate_roster_pdf(sched_act_ids):
r"""Generates a PDF roster for one or more.
:class:`EighthScheduledActivity`\s.
Args
sched_act_ids
The list of IDs of the scheduled activities to show in the PDF.
Returns a BytesIO object for the PDF.
"""
pdf_buffer = io.BytesIO()
h_margin = 1 * inch
v_margin = 0.5 * inch
doc = SimpleDocTemplate(pdf_buffer, pagesize=letter, rightMargin=h_margin, leftMargin=h_margin, topMargin=v_margin, bottomMargin=v_margin)
elements = []
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name="Center", alignment=TA_CENTER))
styles.add(ParagraphStyle(name="BlockLetter", fontSize=60, leading=72, alignment=TA_CENTER))
styles.add(ParagraphStyle(name="BlockLetterSmall", fontSize=30, leading=72, alignment=TA_CENTER))
styles.add(ParagraphStyle(name="BlockLetterSmallest", fontSize=20, leading=72, alignment=TA_CENTER))
styles.add(ParagraphStyle(name="ActivityAttribute", fontSize=15, leading=18, alignment=TA_RIGHT))
for i, said in enumerate(sched_act_ids):
sact = EighthScheduledActivity.objects.get(id=said)
sponsor_names = sact.get_true_sponsors().values_list("first_name", "last_name")
sponsors_str = "; ".join(l + ", " + f for f, l in sponsor_names)
room_names = sact.get_true_rooms().values_list("name", flat=True)
if len(room_names) == 1:
rooms_str = "Room " + room_names[0]
else:
rooms_str = "Rooms: " + ", ".join(r for r in room_names)
block_letter = sact.block.block_letter
if len(block_letter) < 4:
block_letter_width = 1 * inch
block_letter_width += (0.5 * inch) * (len(block_letter) - 1)
block_letter_style = "BlockLetter"
elif len(block_letter) < 7:
block_letter_width = 0.4 * inch
block_letter_width += (0.3 * inch) * (len(block_letter) - 1)
block_letter_style = "BlockLetterSmall"
else:
block_letter_width = 0.3 * inch
block_letter_width += (0.2 * inch) * (len(block_letter) - 1)
block_letter_style = "BlockLetterSmallest"
header_data = [
[
Paragraph("<b>Activity ID: {}<br/>Scheduled ID: {}</b>".format(sact.activity.id, sact.id), styles["Normal"]),
Paragraph(
"{}<br/>{}<br/>{}".format(sponsors_str, rooms_str, sact.block.date.strftime("%A, %B %-d, %Y")), styles["ActivityAttribute"]
),
Paragraph(block_letter, styles[block_letter_style]),
]
]
header_style = TableStyle(
[
("VALIGN", (0, 0), (0, 0), "TOP"),
("VALIGN", (1, 0), (2, 0), "MIDDLE"),
("TOPPADDING", (0, 0), (0, 0), 15),
("RIGHTPADDING", (1, 0), (1, 0), 0),
]
)
elements.append(Table(header_data, style=header_style, colWidths=[2 * inch, None, block_letter_width]))
elements.append(Spacer(0, 10))
elements.append(Paragraph(sact.full_title, styles["Title"]))
num_members = sact.members.count()
num_members_label = "{} Student{}".format(num_members, "s" if num_members != 1 else "")
elements.append(Paragraph(num_members_label, styles["Center"]))
elements.append(Spacer(0, 5))
attendance_data = [
[Paragraph("Present", styles["Heading5"]), Paragraph("Student Name (ID)", styles["Heading5"]), Paragraph("Grade", styles["Heading5"])]
]
members = []
for member in sact.members.all():
members.append(
(
member.last_name + ", " + member.first_name,
(member.student_id if member.student_id else "User {}".format(member.id)),
int(member.grade) if member.grade else "?",
)
)
members = sorted(members)
for member_name, member_id, member_grade in members:
row = ["", "{} ({})".format(member_name, member_id), member_grade]
attendance_data.append(row)
# Line commands are like this:
# op, start, stop, weight, colour, cap, dashes, join, linecount, linespacing
attendance_style = TableStyle(
[
("LINEABOVE", (0, 1), (2, 1), 1, colors.black, None, None, None, 2),
("LINEBELOW", (0, 1), (0, len(attendance_data)), 1, colors.black),
("TOPPADDING", (0, 1), (-1, -1), 6),
("BOTTOMPADDING", (0, 1), (-1, -1), 0),
("BOTTOMPADDING", (0, 0), (-1, 0), 5),
]
)
elements.append(Table(attendance_data, style=attendance_style, colWidths=[1.3 * inch, None, 0.8 * inch]))
elements.append(Spacer(0, 15))
# NOTE: We should really not be writing raw HTML
instructions = """
<b>Highlight or circle</b> the names of students who are <b>absent</b>, and put an <b>"X"</b> next to those <b>present</b>.<br/>
If a student arrives and their name is not on the roster, please send them to the <b>8th Period Office</b>.<br/>
If a student leaves your activity early, please make a note. <b>Do not make any additions to the roster.</b><br/>
Before leaving for the day, return the roster and any passes to 8th Period coordinator, {}'s mailbox in the
<b>main office</b>. For questions, please call extension 5046 or 5078. Thank you!<br/>""".format(
escape(settings.EIGHTH_COORDINATOR_NAME)
)
elements.append(Paragraph(instructions, styles["Normal"]))
if i != len(sched_act_ids) - 1:
elements.append(PageBreak())
def first_page(canvas, _):
canvas.setTitle("Eighth Activity Roster")
canvas.setAuthor("Generated by Ion")
doc.build(elements, onFirstPage=first_page)
return pdf_buffer
@login_required
@deny_restricted
def eighth_absences_view(request, user_id=None):
if user_id and request.user.is_eighth_admin:
user = get_object_or_404(get_user_model(), id=user_id)
elif "user" in request.GET and request.user.is_eighth_admin:
user = get_object_or_404(get_user_model(), id=request.GET["user"])
else:
if request.user.is_student:
user = request.user
else:
return redirect("eighth_admin_dashboard")
absences = (
EighthSignup.objects.filter(user=user, was_absent=True, scheduled_activity__attendance_taken=True)
.select_related("scheduled_activity__block", "scheduled_activity__activity")
.order_by("scheduled_activity__block")
)
context = {"absences": absences, "user": user}
return render(request, "eighth/absences.html", context)
@login_required
@deny_restricted
def sponsor_schedule_widget_view(request):
user = request.user
eighth_sponsor = user.get_eighth_sponsor()
num_blocks = 6
surrounding_blocks = None
date = None
if "date" in request.GET:
date = decode_date(request.GET.get("date"))
if date:
block = EighthBlock.objects.filter(date__gte=date).first()
if block:
surrounding_blocks = [block] + list(block.next_blocks(num_blocks - 1))
else:
surrounding_blocks = []
if surrounding_blocks is None:
surrounding_blocks = EighthBlock.objects.get_upcoming_blocks(num_blocks)
context = {}
if eighth_sponsor:
sponsor_sch = gen_sponsor_schedule(user, eighth_sponsor, num_blocks, surrounding_blocks, date)
context.update(sponsor_sch)
# "sponsor_schedule", "no_attendance_today", "num_attendance_acts",
# "sponsor_schedule_cur_date", "sponsor_schedule_prev_date", "sponsor_schedule_next_date"
context.update({"eighth_sponsor": eighth_sponsor})
return render(request, "eighth/sponsor_widget.html", context)
@login_required
@deny_restricted
def email_students_view(request, scheduled_activity_id):
scheduled_activity = get_object_or_404(EighthScheduledActivity, id=scheduled_activity_id)
if not scheduled_activity.user_is_sponsor(request.user) and not request.user.is_eighth_admin:
raise Http404
if request.method == "POST" and request.POST.get("body"):
subject = settings.EMAIL_SUBJECT_PREFIX + "{}: Message from {}".format(scheduled_activity, request.user.full_name)
if request.POST.get("subject"):
subject += ": " + request.POST["subject"]
body = """{} has requested to send you the following email regarding {}:\n\n{}""".format(
request.user.full_name, scheduled_activity.activity, request.POST["body"]
)
email_scheduled_activity_students_task.delay(
scheduled_activity_id=scheduled_activity_id,
sender_id=request.user.id,
subject=subject,
body=body,
)
messages.success(request, "Email sent.")
return redirect("eighth_take_attendance", scheduled_activity_id)
context = {"scheduled_activity": scheduled_activity}
return render(request, "eighth/email_students.html", context)