""" Agenda PDF generator Generates an 8.5" x 11" bilingual (EN/FR) weekly planner from Dec 1 2025 to Jan 31 2027. Produces one week per left page and a matching notes page on the right (two pages per week). Dependencies: pip install reportlab python-dateutil holidays Run: python agenda_generator.py Output: ./agenda_2025_2027.pdf Notes: - Margins: 0.5 inch all around - Fonts: Helvetica (headers) and Times (body) — ReportLab built-ins - Colors: green header band approximated. Adjust RGB constants if you prefer a different tone. - Holidays: uses the `holidays` package for Canada federal holidays; displays bilingual labels. - Mini calendars on the notes (right) page show the current month (with the week's days highlighted) and the next month. """ from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.lib.units import inch from reportlab.lib.colors import Color, black, HexColor from datetime import date, timedelta import calendar import holidays from dateutil.relativedelta import relativedelta # ---------- Configuration ---------- PAGE_SIZE = letter # 8.5 x 11 inches MARGIN = 0.5 * inch PAGE_WIDTH, PAGE_HEIGHT = PAGE_SIZE CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN OUTPUT_FILENAME = "agenda_2025_2027.pdf" # Visual styles (adjust if desired) HEADER_GREEN = HexColor('#1f6f5c') # approximate green from sample RULE_GRAY = HexColor('#d7dbe0') TEXT_COLOR = HexColor('#1b1b1b') HEADER_HEIGHT = 0.55 * inch SIDEBAR_WIDTH = 0.6 * inch LINE_SPACING = 0.35 * inch # space per ruled line # Date range START_DATE = date(2025, 12, 1) END_DATE = date(2027, 1, 31) # Bilingual labels DAYS_EN = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] DAYS_FR = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'] PRIORITY_LABEL = ('Priority', 'Priorité') ACTION_LABEL = ('Action Items', "Points d'action") PERSONAL_LABEL = ('Personal', 'Personnel') WEEK_LABEL = ('Week', 'Semaine') # ---------- Utility functions ---------- def daterange(start_date, end_date): d = start_date while d <= end_date: yield d d += timedelta(days=1) def week_starting(date_obj): # We'll define week to start on Monday. Return the Monday of the week containing date_obj. return date_obj - timedelta(days=(date_obj.weekday())) def collect_week_mondays(start_date, end_date): # Collect all Mondays between week_start(start_date) and week_start(end_date) first_monday = week_starting(start_date) last_monday = week_starting(end_date) mondays = [] cur = first_monday while cur <= last_monday: mondays.append(cur) cur += timedelta(days=7) return mondays def format_bilingual_date_range(mon): # mon is a Monday date; return strings like 'December 1-7 / 1-7 décembre' start = mon end = mon + timedelta(days=6) # English: MonthName start_day - end_day en = f"{start.strftime('%B')} {start.day} - {end.day}" # French: day numbers then month (putting month after numbers like sample) fr = f"{start.day} - {end.day} {start.strftime('%B') if False else end.strftime('%B')}" # Note: we'll place month names in English and French; for full localization one could map months to FR names. # Simpler bilingual: put English then French month name (basic) fr_month = end.strftime('%B') return f"{start.strftime('%B')} {start.day} - {end.day} / {start.day} - {end.day} {fr_month}" def month_name_fr(english_month_name): # Minimal mapping of English month names to French versions mapping = { 'January': 'janvier', 'February': 'février', 'March': 'mars', 'April': 'avril', 'May': 'mai', 'June': 'juin', 'July': 'juillet', 'August': 'août', 'September': 'septembre', 'October': 'octobre', 'November': 'novembre', 'December': 'décembre' } return mapping.get(english_month_name, english_month_name) def draw_weekly_left_page(c: canvas.Canvas, monday: date, week_number: int, ca_holidays): # left page layout: Monday..Sunday stacked, vernacular headers, sidebar week label x0 = MARGIN y0 = PAGE_HEIGHT - MARGIN # Header band c.setFillColor(HEADER_GREEN) c.rect(x0, y0 - HEADER_HEIGHT, CONTENT_WIDTH - SIDEBAR_WIDTH, HEADER_HEIGHT, stroke=0, fill=1) # Header text (bilingual date range) c.setFillColor('white') c.setFont('Helvetica-Bold', 12) date_range_str = format_bilingual_date_range(monday) c.drawString(x0 + 0.15 * inch, y0 - HEADER_HEIGHT + 0.15 * inch, date_range_str) # Sidebar vertical band for week label sb_x = x0 + CONTENT_WIDTH - SIDEBAR_WIDTH c.setFillColor(HEADER_GREEN) c.rect(sb_x, y0 - CONTENT_HEIGHT, SIDEBAR_WIDTH, CONTENT_HEIGHT, stroke=0, fill=1) # Week label (vertical) c.saveState() c.translate(sb_x + SIDEBAR_WIDTH / 2, y0 - HEADER_HEIGHT - 0.1 * inch) c.rotate(90) c.setFillColor('white') c.setFont('Helvetica', 9) month_en = monday.strftime('%B') month_fr = month_name_fr(month_en) label = f"{WEEK_LABEL[0]} {week_number} {month_en} / {WEEK_LABEL[1]} {week_number} {month_fr}" c.drawCentredString(0, 0, label) c.restoreState() # Draw ruled lines for each day area day_x = x0 + 0.15 * inch day_width = CONTENT_WIDTH - SIDEBAR_WIDTH - 0.3 * inch top_y = y0 - HEADER_HEIGHT - 0.15 * inch c.setStrokeColor(RULE_GRAY) c.setLineWidth(0.6) # space allocation: divide remaining height into 7 equal day blocks remaining_height = CONTENT_HEIGHT - HEADER_HEIGHT - 0.3 * inch day_block_h = remaining_height / 7.0 c.setFont('Helvetica', 9) for i in range(7): dy_top = top_y - i * day_block_h # day header text day_date = monday + timedelta(days=i) day_label = f"{DAYS_EN[i]} / {DAYS_FR[i]} — {day_date.strftime('%b %d')}" c.setFillColor(TEXT_COLOR) c.drawString(day_x, dy_top - 12, day_label) # draw ruled lines inside day block num_lines = int((day_block_h - 16) / LINE_SPACING) y_line = dy_top - 18 for l in range(num_lines): c.setStrokeColor(RULE_GRAY) c.line(day_x, y_line - l * LINE_SPACING, day_x + day_width, y_line - l * LINE_SPACING) # priority label under the day header (top of block) c.setFont('Times-Roman', 8) c.setFillColor(HexColor('#4b4b4b')) c.drawString(day_x + day_width - 1.6 * inch, dy_top - 12, f"{PRIORITY_LABEL[0]} / {PRIORITY_LABEL[1]}") c.setFont('Helvetica', 9) # Mark holiday if present if day_date in ca_holidays: hol = ca_holidays.get(day_date) # Holidays package returns English names by default; display bilingual by simple duplication hol_fr = hol c.setFillColor(HexColor('#b02b2b')) c.drawString(day_x + 0.15 * inch, dy_top - 26, f"{hol} / {hol_fr}") c.setFillColor(TEXT_COLOR) # finished left page def draw_small_month_calendar(c: canvas.Canvas, x, y, year, month, highlight_week_dates): # Draw a compact calendar box with month title, weekday initials and day numbers. # highlight_week_dates: set of dates to highlight (day numbers) cal = calendar.Calendar(firstweekday=6) # Sunday-first for mini-cal (visual familiar) month_days = cal.monthdayscalendar(year, month) box_w = 1.6 * inch box_h = 1.6 * inch c.setStrokeColor(black) c.rect(x, y - box_h, box_w, box_h, stroke=1, fill=0) c.setFont('Helvetica-Bold', 7) month_title = f"{calendar.month_name[month]} {year}" c.drawCentredString(x + box_w / 2, y - 8, month_title) # weekday initials row c.setFont('Helvetica', 6) days = ['S','M','T','W','T','F','S'] cell_w = box_w / 7.0 top = y - 20 for i, d in enumerate(days): c.drawString(x + i * cell_w + 2, top, d) # day numbers c.setFont('Helvetica', 6) row_y = top - 10 for wk in month_days: for i, daynum in enumerate(wk): if daynum == 0: continue cell_x = x + i * cell_w + 2 # highlight if in highlight_week_dates if daynum in highlight_week_dates: c.setFillColor(HEADER_GREEN) c.rect(cell_x - 1, row_y - 2, cell_w - 2, 8, stroke=0, fill=1) c.setFillColor('white') c.drawString(cell_x + 1, row_y - 1, str(daynum)) c.setFillColor(TEXT_COLOR) else: c.drawString(cell_x + 1, row_y - 1, str(daynum)) row_y -= 10 def draw_notes_right_page(c: canvas.Canvas, monday: date, ca_holidays): # Right page layout: large action items area, 'Personal' area at bottom, mini-calendars x0 = MARGIN y0 = PAGE_HEIGHT - MARGIN # Header band c.setFillColor(HEADER_GREEN) c.rect(x0, y0 - HEADER_HEIGHT, CONTENT_WIDTH, HEADER_HEIGHT, stroke=0, fill=1) c.setFillColor('white') c.setFont('Helvetica-Bold', 12) c.drawString(x0 + 0.15 * inch, y0 - HEADER_HEIGHT + 0.15 * inch, f"{ACTION_LABEL[0]} / {ACTION_LABEL[1]}") # Large ruled area for action items top_y = y0 - HEADER_HEIGHT - 0.15 * inch area_h = CONTENT_HEIGHT * 0.6 c.setStrokeColor(RULE_GRAY) num_lines = int(area_h / LINE_SPACING) c.setFont('Times-Roman', 9) for i in range(num_lines): y_line = top_y - i * LINE_SPACING c.line(x0 + 0.15 * inch, y_line, x0 + CONTENT_WIDTH - 0.15 * inch - 1.8 * inch, y_line) # Personal section divider personal_y = top_y - area_h - 0.15 * inch c.setFont('Helvetica', 9) c.setFillColor(TEXT_COLOR) c.drawString(x0 + 0.15 * inch, personal_y, f"{PERSONAL_LABEL[0]} / {PERSONAL_LABEL[1]}") # Personal ruled area personal_area_h = CONTENT_HEIGHT * 0.35 num_lines = int(personal_area_h / LINE_SPACING) for i in range(num_lines): y_line = personal_y - 12 - i * LINE_SPACING c.line(x0 + 0.15 * inch, y_line, x0 + CONTENT_WIDTH - 0.15 * inch - 1.8 * inch, y_line) # Mini calendars at bottom-right # Determine current month and next month based on the monday (week) # We'll highlight days that belong to this week week_dates = [monday + timedelta(days=i) for i in range(7)] curr_month = week_dates[0].month curr_year = week_dates[0].year # But a week may cross month boundaries; choose the month that contains Thursday (common week-month rule) thursday = monday + timedelta(days=3) curr_month = thursday.month curr_year = thursday.year next_month_date = (thursday + relativedelta(months=1)).replace(day=1) # set of day numbers in current month to highlight highlight_days = set([d.day for d in week_dates if d.month == curr_month and d.year == curr_year]) cal_x = x0 + CONTENT_WIDTH - 1.8 * inch cal_y = MARGIN + 1.9 * inch draw_small_month_calendar(c, cal_x, cal_y + 1.8 * inch, curr_year, curr_month, highlight_days) draw_small_month_calendar(c, cal_x, cal_y - 0.1 * inch, next_month_date.year, next_month_date.month, set()) # tiny vertical tab for month (like sample) tab_x = x0 + CONTENT_WIDTH - 0.28 * inch c.setFillColor(HEADER_GREEN) c.rect(tab_x, cal_y - 0.1 * inch, 0.28 * inch, 0.9 * inch, stroke=0, fill=1) c.setFillColor('white') c.setFont('Helvetica', 6) month_label = month_name_fr(calendar.month_name[curr_month]).capitalize() c.drawCentredString(tab_x + 0.14 * inch, cal_y + 0.3 * inch, month_label) def main(): # Prepare holidays for the span years years = list(range(START_DATE.year, END_DATE.year + 1)) ca_holidays = holidays.CA(years=years) c = canvas.Canvas(OUTPUT_FILENAME, pagesize=PAGE_SIZE) mondays = collect_week_mondays(START_DATE, END_DATE) for idx, mon in enumerate(mondays, start=1): week_num = mon.isocalendar()[1] # Left page (weekly) draw_weekly_left_page(c, mon, week_num, ca_holidays) c.showPage() # Right page (notes) draw_notes_right_page(c, mon, ca_holidays) c.showPage() c.save() print(f"Saved: {OUTPUT_FILENAME}") if __name__ == '__main__': main()