docs: def generate_roster_pdf(sched_act_ids, include_instructions):
      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.
          include_instructions
              Whether instructions should be printed at the bottom of the
              roster(s).
  
      Returns a BytesIO object for the PDF.
  
      """
  
      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="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))
          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, Catherine Forrester's mailbox in the
          <b>main office</b>. For questions, please call extension 5046 or 5078. Thank you!<br />"""
  
          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