Source code for intranet.apps.eighth.views.activities

import csv
import logging
from collections import defaultdict
from datetime import MAXYEAR, MINYEAR, date, datetime, timedelta
from io import BytesIO

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 Count
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
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

from ....utils.date import get_date_range_this_year, get_senior_graduation_year
from ....utils.helpers import is_entirely_digit
from ....utils.serialization import safe_json
from ...auth.decorators import deny_restricted
from ..forms.activities import ActivitySettingsForm
from ..forms.admin.activities import ActivityMultiSelectForm
from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor
from ..utils import get_start_date

logger = logging.getLogger(__name__)


@login_required
@deny_restricted
def activity_view(request, activity_id=None):
    activity = get_object_or_404(EighthActivity, id=activity_id)
    scheduled_activities = EighthScheduledActivity.objects.filter(activity=activity)

    show_all = "show_all" in request.GET
    if not show_all:
        first_date = timezone.localtime(timezone.now()).date()
        first_block = EighthBlock.objects.get_first_upcoming_block()
        if first_block:
            first_date = first_block.date

        two_months = timezone.localtime(timezone.now()).date() + timedelta(weeks=8)
        scheduled_activities = scheduled_activities.filter(block__date__gte=first_date, block__date__lte=two_months)

    scheduled_activities = scheduled_activities.order_by("block__date", "block__block_letter")

    context = {"activity": activity, "scheduled_activities": scheduled_activities}

    return render(request, "eighth/activity.html", context)


[docs]@login_required def settings_view(request, activity_id=None): activity = get_object_or_404(EighthActivity, id=activity_id) if not (EighthSponsor.objects.filter(user=request.user).exists() or request.user in activity.club_sponsors.all()): raise Http404 if request.method == "POST": form = ActivitySettingsForm(request.POST, instance=activity, sponsors=activity.sponsors.all()) if form.is_valid(): form.save() else: messages.error(request, "There was an error saving the activity settings.") else: form = ActivitySettingsForm(instance=activity, sponsors=activity.sponsors.all()) context = {"activity": activity, "form": form} return render(request, "eighth/activity_settings.html", context)
[docs]def chunks(items, n): for i in range(0, len(items), n): yield items[i : i + n]
[docs]def current_school_year(): now = timezone.localtime() if now.month <= settings.YEAR_TURNOVER_MONTH: return now.year else: return now.year + 1
[docs]def generate_statistics_pdf(activities=None, start_date=None, all_years=False, year=None): """Accepts EighthActivity objects and outputs a PDF file.""" if activities is None: activities = EighthActivity.objects.all().order_by("name") if year is None: year = current_school_year() if not isinstance(activities, list): activities = activities.prefetch_related("rooms").prefetch_related("sponsors") pdf_buffer = 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="Indent", leftIndent=15)) empty_activities = [] for act in activities: lelements = [] relements = [] act_stats = calculate_statistics(act, start_date=start_date, all_years=all_years, year=year) if act_stats["total_blocks"] == 0: empty_activities.append(act.name) continue elements.append(Paragraph(act.name, styles["Title"])) sponsor_str = (", ".join([x.name for x in act.sponsors.all()])) if act.sponsors.count() > 0 else "None" lelements.append(Paragraph("<b>Default Sponsors:</b> " + sponsor_str, styles["Normal"])) lelements.append(Spacer(0, 0.025 * inch)) room_str = (", ".join([str(x) for x in act.rooms.all()])) if act.rooms.count() > 0 else "None" relements.append(Paragraph("<b>Default Rooms:</b> " + room_str, styles["Normal"])) relements.append(Spacer(0, 0.025 * inch)) relements.append(Paragraph("<b>Total blocks:</b> {}".format(act_stats["total_blocks"]), styles["Normal"])) relements.append(Paragraph("<b>Scheduled blocks:</b> {}".format(act_stats["scheduled_blocks"]), styles["Indent"])) relements.append(Paragraph("<b>Empty blocks:</b> {}".format(act_stats["empty_blocks"]), styles["Indent"])) relements.append(Paragraph("<b>Cancelled blocks:</b> {}".format(act_stats["cancelled_blocks"]), styles["Indent"])) lelements.append(Paragraph("<b>Total signups:</b> {}".format(act_stats["total_signups"]), styles["Normal"])) lelements.append(Paragraph("<b>Average signups per block:</b> {}".format(act_stats["average_signups"]), styles["Indent"])) lelements.append(Paragraph("<b>Average signups per student:</b> {}".format(act_stats["average_user_signups"]), styles["Indent"])) lelements.append( Paragraph("<b>Unique students:</b> {}, <b>Capacity:</b> {}".format(act_stats["students"], act_stats["capacity"]), styles["Normal"]) ) elements.append( Table( [[lelements, relements]], style=[("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0), ("VALIGN", (0, 0), (-1, -1), "TOP")], ) ) parsed_members = [[x.username, y] for x, y in act_stats["members"]] parsed_members = list(chunks(parsed_members, 30))[:3] if parsed_members: parsed_members = [[["Username", "Signups"]] + x for x in parsed_members] parsed_members = [ Table(x, style=[("FONT", (0, 0), (1, 0), "Helvetica-Bold"), ("ALIGN", (1, 0), (1, -1), "RIGHT")]) for x in parsed_members ] elements.append(Table([parsed_members], style=[("VALIGN", (-1, -1), (-1, -1), "TOP")])) if act_stats["students"] - 90 > 0: elements.append(Paragraph("<b>{}</b> students were not shown on this page. ".format(act_stats["students"] - 90), styles["Normal"])) else: elements.append(Spacer(0, 0.20 * inch)) if start_date is not None: elements.append( Paragraph( "<b>{}</b> block(s) are past the start date and are not included on this page.".format(act_stats["past_start_date"]), styles["Normal"], ) ) elements.append( Paragraph( "<b>{}</b> block(s) not in the {}-{} school year are not included on this page.".format(act_stats["old_blocks"], year - 1, year), styles["Normal"], ) ) elements.append(PageBreak()) if empty_activities: empty_activities = [x[:37] + "..." if len(x) > 40 else x for x in empty_activities] empty_activities = [[x] for x in empty_activities] empty_activities = list(chunks(empty_activities, 35)) empty_activities = [[["Activity"]] + x for x in empty_activities] empty_activities = [ Table(x, style=[("FONT", (0, 0), (-1, 0), "Helvetica-Bold"), ("LEFTPADDING", (0, 0), (-1, -1), 0)]) for x in empty_activities ] for i in range(0, len(empty_activities), 2): elements.append(Paragraph(f"Empty Activities (Page {i // 2 + 1})", styles["Title"])) if all_years: elements.append(Paragraph("The following activities have no 8th period blocks assigned to them.", styles["Normal"])) else: elements.append( Paragraph( f"The following activities have no 8th period blocks assigned to them for the {year - 1}-{year} school year.", styles["Normal"], ) ) elements.append(Spacer(0, 0.10 * inch)) ea = [empty_activities[i]] if i + 1 < len(empty_activities): ea.append(empty_activities[i + 1]) elements.append( Table( [ea], style=[("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0), ("VALIGN", (0, 0), (-1, -1), "TOP")], hAlign="LEFT", ) ) elements.append(PageBreak()) def first_page(canvas, _): if len(activities) == 1: canvas.setTitle(f"{activities[0].name} Statistics") else: canvas.setTitle("8th Period Activity Statistics") canvas.setAuthor("Generated by Ion") doc.build(elements, onFirstPage=first_page) pdf_buffer.seek(0) return pdf_buffer
[docs]def calculate_statistics(activity, start_date=None, all_years=False, year=None, future=False): activities = EighthScheduledActivity.objects.filter(activity=activity) signups = defaultdict(int) chart_data = defaultdict(dict) old_blocks = 0 if year is not None and year == get_senior_graduation_year(): start_date = datetime.today() if start_date is None or future: filtered_activities = activities past_start_date = 0 else: filtered_activities = activities.filter(block__date__lte=start_date) past_start_date = activities.count() - filtered_activities.count() if not all_years: if year is None or year == get_senior_graduation_year(): year_start, year_end = get_date_range_this_year() else: year_start, year_end = get_date_range_this_year(datetime(year, 1, 1)) year_filtered = filtered_activities.filter(block__date__gte=year_start, block__date__lte=year_end) old_blocks = filtered_activities.count() - year_filtered.count() filtered_activities = year_filtered activities = filtered_activities cancelled_blocks = activities.filter(cancelled=True).count() empty_blocks = 0 for a in activities.filter(cancelled=False).select_related("block").prefetch_related("members"): members = a.members.filter(eighthsignup__was_absent=False).count() if members == 0: empty_blocks += 1 else: for user in a.members.all(): signups[user] += 1 chart_data[str(a.block.date)][str(a.block.block_letter)] = members signups = sorted(signups.items(), key=lambda kv: (-kv[1], kv[0].username)) total_blocks = activities.count() scheduled_blocks = total_blocks - cancelled_blocks total_signups = sum(n for _, n in signups) if scheduled_blocks: average_signups = round(total_signups / scheduled_blocks, 2) else: average_signups = 0 if signups: average_user_signups = round(total_signups / len(signups), 2) else: average_user_signups = 0 return { "members": signups, "students": len(signups), "total_blocks": total_blocks, "total_signups": total_signups, "average_signups": average_signups, "average_user_signups": average_user_signups, "old_blocks": old_blocks, "cancelled_blocks": cancelled_blocks, "scheduled_blocks": scheduled_blocks, "empty_blocks": empty_blocks, "capacity": activities[total_blocks - 1].get_true_capacity() if total_blocks > 0 else 0, "chart_data": safe_json(chart_data), "past_start_date": past_start_date, }
@login_required @deny_restricted def stats_global_view(request): if not request.user.is_eighth_admin: return render(request, "error/403.html", {"reason": "You do not have permission to generate global statistics."}, status=403) if request.method == "POST" and request.POST.get("year", False): year = int(request.POST.get("year")) do_csv = request.POST.get("generate", "csv") == "csv" if do_csv: response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="eighth.csv"' writer = csv.writer(response) writer.writerow( [ "Activity", "Default Sponsors", "Default Rooms", "Capacity", "Unique Students", "Total Blocks", "Scheduled Blocks", "Cancelled Blocks", "Empty Blocks", "Total Signups", "Average Signups per Block", "Average Signups per Student", ] ) for act in EighthActivity.objects.all().order_by("name").prefetch_related("rooms").prefetch_related("sponsors"): stats = calculate_statistics(act, year=year) writer.writerow( [ act.name, ", ".join([x.name for x in act.sponsors.all()]), ", ".join([str(x) for x in act.rooms.all()]), stats["capacity"], stats["students"], stats["total_blocks"], stats["scheduled_blocks"], stats["cancelled_blocks"], stats["empty_blocks"], stats["total_signups"], stats["average_signups"], stats["average_user_signups"], ] ) else: response = HttpResponse(content_type="application/pdf") response["Content-Disposition"] = 'inline; filename="eighth.pdf"' buf = generate_statistics_pdf(year=year) response.write(buf.getvalue()) buf.close() return response current_year = current_school_year() if EighthBlock.objects.count() == 0: earliest_year = current_year else: earliest_year = EighthBlock.objects.order_by("date").first().date.year context = {"years": list(reversed(range(earliest_year, current_year + 1)))} return render(request, "eighth/admin/global_statistics.html", context) @login_required @deny_restricted def stats_multiple_view(request): if not request.user.is_eighth_admin: return render(request, "error/403.html", {"reason": "You do not have permission to view eighth activity statistics."}, status=403) form = None context = None if request.method == "POST": form = ActivityMultiSelectForm(request.POST) lower_absence_limit = request.POST.get("lower", "") upper_absence_limit = request.POST.get("upper", "") include_freshmen = request.POST.get("freshmen", "off") == "on" include_sophomores = request.POST.get("sophomores", "off") == "on" include_juniors = request.POST.get("juniors", "off") == "on" include_seniors = request.POST.get("seniors", "off") == "on" start_date = request.POST.get("start", "") end_date = request.POST.get("end", "") if lower_absence_limit == "" or not is_entirely_digit(lower_absence_limit): lower_absence_limit = "1" lower_absence_limit_filter = 1 else: lower_absence_limit_filter = lower_absence_limit if upper_absence_limit == "" or not is_entirely_digit(upper_absence_limit): upper_absence_limit = "100" upper_absence_limit_filter = 100 else: upper_absence_limit_filter = upper_absence_limit try: start_date = datetime.strptime(start_date, "%Y-%m-%d") start_date_filter = start_date except ValueError: start_date = "" start_date_filter = date(MINYEAR, 1, 1) try: end_date = datetime.strptime(end_date, "%Y-%m-%d") end_date_filter = end_date except ValueError: end_date = "" end_date_filter = date(MAXYEAR, 12, 31) context = { "lower_absence_limit": lower_absence_limit, "upper_absence_limit": upper_absence_limit, "include_freshmen": include_freshmen, "include_sophomores": include_sophomores, "include_juniors": include_juniors, "include_seniors": include_seniors, "start_date": start_date, "end_date": end_date, "form": form, "admin_page_title": "View activity signup statistics", } if form.is_valid(): if int(upper_absence_limit_filter) > 0 and int(lower_absence_limit_filter) > 0: act_ids = [act.id for act in form.cleaned_data["activities"]] signed_up = ( EighthSignup.objects.filter( scheduled_activity__block__date__gte=start_date_filter, scheduled_activity__block__date__lte=end_date_filter, scheduled_activity__activity__id__in=act_ids, ) .values("user") .annotate(signups=Count("user")) .filter(signups__gte=lower_absence_limit_filter, signups__lte=upper_absence_limit_filter) .order_by("user") .values("user", "signups") ) user_ids = [d["user"] for d in signed_up] signed_up_users = get_user_model().objects.filter(id__in=user_ids).order_by("id") for index, user in enumerate(signed_up_users): signed_up[index]["user"] = user signed_up = list(signed_up) def filter_by_grade(signed_up): grade = signed_up["user"].grade.number include = False if include_freshmen: include |= grade == 9 if include_sophomores: include |= grade == 10 if include_juniors: include |= grade == 11 if include_seniors: include |= grade == 12 return include signed_up = list(filter(filter_by_grade, signed_up)) signed_up = sorted(signed_up, key=lambda x: (-1 * x["signups"], x["user"].last_name)) context["signed_up"] = signed_up else: form = ActivityMultiSelectForm() context = {"form": form, "admin_page_title": "View activity signup statistics"} return render(request, "eighth/admin/multiple_statistics.html", context=context) @login_required @deny_restricted def stats_view(request, activity_id=None): """If a the GET parameter `year` is set, it uses stats from given year with the following caveats: - If it's the current year and start_date is set, start_date is ignored - If it's the current year, stats will only show up to today - they won't go into the future. `all_years` (obviously) displays all years. """ if not (request.user.is_eighth_admin or request.user.is_teacher): return render(request, "error/403.html", {"reason": "You do not have permission to view statistics for this activity."}, status=403) activity = get_object_or_404(EighthActivity, id=activity_id) if request.GET.get("print", False): response = HttpResponse(content_type="application/pdf") buf = generate_statistics_pdf([activity], year=int(request.GET.get("year", 0)) or None) response.write(buf.getvalue()) buf.close() return response current_year = current_school_year() if EighthBlock.objects.count() == 0: earliest_year = current_year else: earliest_year = EighthBlock.objects.order_by("date").first().date.year if request.GET.get("year", False): year = int(request.GET.get("year")) else: year = None future = request.GET.get("future", False) context = {"activity": activity, "years": list(reversed(range(earliest_year, current_year + 1))), "year": year, "future": future} if year: context.update(calculate_statistics(activity, year=year, future=future)) else: context.update(calculate_statistics(activity, get_start_date(request), future=future)) return render(request, "eighth/statistics.html", context)