[32788f42] feat: implement database persistence, modernized UI with Tailwind, and Calendly-integrated QR card generator for Fotograf.de scraper
This commit is contained in:
200
fotograf-de-scraper/backend/qr_generator.py
Normal file
200
fotograf-de-scraper/backend/qr_generator.py
Normal file
@@ -0,0 +1,200 @@
|
||||
import os
|
||||
import requests
|
||||
import io
|
||||
import datetime
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("qr-card-generator")
|
||||
|
||||
def get_calendly_events_raw(api_token: str, start_time: str, end_time: str, event_type_name: str = None):
|
||||
"""
|
||||
Debug function to fetch raw Calendly data without formatting.
|
||||
"""
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# 1. Get current user info to get the user URI
|
||||
user_url = "https://api.calendly.com/users/me"
|
||||
user_response = requests.get(user_url, headers=headers)
|
||||
if not user_response.ok:
|
||||
raise Exception(f"Calendly API Error: {user_response.status_code}")
|
||||
|
||||
user_data = user_response.json()
|
||||
user_uri = user_data['resource']['uri']
|
||||
|
||||
# 2. Get events for the user
|
||||
events_url = "https://api.calendly.com/scheduled_events"
|
||||
params = {
|
||||
'user': user_uri,
|
||||
'min_start_time': start_time,
|
||||
'max_start_time': end_time,
|
||||
'status': 'active'
|
||||
}
|
||||
|
||||
events_response = requests.get(events_url, headers=headers, params=params)
|
||||
if not events_response.ok:
|
||||
raise Exception(f"Calendly API Error: {events_response.status_code}")
|
||||
|
||||
events_data = events_response.json()
|
||||
events = events_data['collection']
|
||||
|
||||
raw_results = []
|
||||
|
||||
# 3. Get invitees
|
||||
for event in events:
|
||||
event_name = event.get('name', '')
|
||||
# Filter by event type if provided
|
||||
if event_type_name and event_type_name.lower() not in event_name.lower():
|
||||
continue
|
||||
|
||||
event_uri = event['uri']
|
||||
event_uuid = event_uri.split('/')[-1]
|
||||
invitees_url = f"https://api.calendly.com/scheduled_events/{event_uuid}/invitees"
|
||||
|
||||
invitees_response = requests.get(invitees_url, headers=headers)
|
||||
if not invitees_response.ok:
|
||||
continue
|
||||
|
||||
invitees_data = invitees_response.json()
|
||||
|
||||
for invitee in invitees_data['collection']:
|
||||
raw_results.append({
|
||||
"event_name": event_name,
|
||||
"start_time": event['start_time'],
|
||||
"invitee_name": invitee['name'],
|
||||
"invitee_email": invitee['email'],
|
||||
"questions_and_answers": invitee.get('questions_and_answers', [])
|
||||
})
|
||||
|
||||
return raw_results
|
||||
|
||||
def get_calendly_events(api_token: str, start_time: str, end_time: str, event_type_name: str = None):
|
||||
"""
|
||||
Fetches events from Calendly API for the current user within a time range.
|
||||
"""
|
||||
raw_data = get_calendly_events_raw(api_token, start_time, end_time, event_type_name)
|
||||
formatted_data = []
|
||||
|
||||
for item in raw_data:
|
||||
# Parse start time
|
||||
start_dt = datetime.datetime.fromisoformat(item['start_time'].replace('Z', '+00:00'))
|
||||
# Format as HH:MM
|
||||
time_str = start_dt.strftime('%H:%M')
|
||||
|
||||
name = item['invitee_name']
|
||||
|
||||
# Extract specific answers from the Calendly form
|
||||
# We look for the number of children and any additional notes
|
||||
num_children = ""
|
||||
additional_notes = ""
|
||||
questions_and_answers = item.get('questions_and_answers', [])
|
||||
|
||||
for q_a in questions_and_answers:
|
||||
q_text = q_a.get('question', '').lower()
|
||||
a_text = q_a.get('answer', '')
|
||||
|
||||
if "wie viele kinder" in q_text:
|
||||
num_children = a_text
|
||||
elif "nachricht" in q_text or "anmerkung" in q_text:
|
||||
# If there's a custom notes field in some events
|
||||
additional_notes = a_text
|
||||
|
||||
# Construct the final string: "Name, X Kinder // HH:MM Uhr (Notes)"
|
||||
# matching: Halime Türe, 1 Kind // 12:00 Uhr
|
||||
final_text = f"{name}"
|
||||
if num_children:
|
||||
final_text += f", {num_children}"
|
||||
|
||||
final_text += f" // {time_str} Uhr"
|
||||
|
||||
if additional_notes:
|
||||
final_text += f" ({additional_notes})"
|
||||
|
||||
formatted_data.append(final_text)
|
||||
|
||||
logger.info(f"Processed {len(formatted_data)} invitees.")
|
||||
return formatted_data
|
||||
|
||||
|
||||
def overlay_text_on_pdf(base_pdf_path: str, output_pdf_path: str, texts: list):
|
||||
"""
|
||||
Overlays text from the `texts` list onto a base PDF.
|
||||
Expects two text entries per page (top and bottom element).
|
||||
Coordinates are in mm from bottom-left (ReportLab default).
|
||||
Target:
|
||||
Element 1: X: 72mm, Y: 22mm (from top-left in user spec, need to convert)
|
||||
Element 2: X: 72mm, Y: 171mm (from top-left in user spec, need to convert)
|
||||
"""
|
||||
|
||||
# Convert mm to points (1 mm = 2.83465 points)
|
||||
mm_to_pt = 2.83465
|
||||
|
||||
# A4 dimensions in points (approx 595.27 x 841.89)
|
||||
page_width, page_height = A4
|
||||
|
||||
# User coordinates are from top-left.
|
||||
# ReportLab uses bottom-left as (0,0).
|
||||
# Element 1 (Top): X = 72mm, Y = 22mm (from top) -> Y = page_height - 22mm
|
||||
# Element 2 (Bottom): X = 72mm, Y = 171mm (from top) -> Y = page_height - 171mm
|
||||
|
||||
x_pos = 72 * mm_to_pt
|
||||
y_pos_1 = page_height - (22 * mm_to_pt)
|
||||
y_pos_2 = page_height - (171 * mm_to_pt)
|
||||
|
||||
reader = PdfReader(base_pdf_path)
|
||||
writer = PdfWriter()
|
||||
|
||||
total_pages = len(reader.pages)
|
||||
max_capacity = total_pages * 2
|
||||
|
||||
if len(texts) > max_capacity:
|
||||
logger.warning(f"Not enough pages in base PDF. Have {len(texts)} invitees but only space for {max_capacity}. Truncating.")
|
||||
texts = texts[:max_capacity]
|
||||
|
||||
# We need to process pairs of texts for each page
|
||||
text_pairs = [texts[i:i+2] for i in range(0, len(texts), 2)]
|
||||
|
||||
for page_idx, pair in enumerate(text_pairs):
|
||||
if page_idx >= total_pages:
|
||||
break # Should be caught by the truncation above, but safety first
|
||||
|
||||
# Create a new blank page in memory to draw the text
|
||||
packet = io.BytesIO()
|
||||
can = canvas.Canvas(packet, pagesize=A4)
|
||||
|
||||
# Draw the text.
|
||||
can.setFont("Helvetica", 12)
|
||||
|
||||
if len(pair) > 0:
|
||||
can.drawString(x_pos, y_pos_1, pair[0])
|
||||
if len(pair) > 1:
|
||||
can.drawString(x_pos, y_pos_2, pair[1])
|
||||
|
||||
can.save()
|
||||
packet.seek(0)
|
||||
|
||||
# Read the text PDF we just created
|
||||
new_pdf = PdfReader(packet)
|
||||
text_page = new_pdf.pages[0]
|
||||
|
||||
# Get the specific page from the original PDF
|
||||
page_to_merge = reader.pages[page_idx]
|
||||
page_to_merge.merge_page(text_page)
|
||||
|
||||
writer.add_page(page_to_merge)
|
||||
|
||||
# If there are pages left in the base PDF that we didn't use, append them too?
|
||||
# Usually you'd want to keep them or discard them. We'll discard unused pages for now
|
||||
# to avoid empty cards, or you can change this loop to include them.
|
||||
|
||||
with open(output_pdf_path, "wb") as output_file:
|
||||
writer.write(output_file)
|
||||
|
||||
logger.info(f"Successfully generated overlaid PDF at {output_pdf_path}")
|
||||
Reference in New Issue
Block a user