Add custom title and sub-headers features\n\n- Add --title option to set custom title instead of default 'BINGO'\n- Add --sub-headers option to add category descriptions below column headers\n- Update main.py to handle new command-line options\n- Update PDF generator to render custom title and sub-headers\n- Add tests for new features\n- Update README.md with documentation for new features

This commit is contained in:
2025-12-07 14:57:59 -05:00
parent de4140bf02
commit c119eb513e
4 changed files with 151 additions and 25 deletions

View File

@@ -44,6 +44,8 @@ python -m src.custom_bingo --input-file <input_file> --output-file <output_file>
- `-i, --input-file PATH`: Input spreadsheet file (Excel/CSV) with B, I, N, G, O columns [required]
- `-o, --output-file PATH`: Output PDF file for the BINGO cards [required]
- `-n, --number-of-cards INTEGER`: Number of BINGO cards to generate (default: 1)
- `-t, --title TEXT`: Title text to display at the top of the card (default: BINGO)
- `-s, --sub-headers TEXT`: Comma-separated sub-headers for each column (e.g., "Category1,Category2,Category3,Category4,Category5")
### Example
@@ -53,6 +55,9 @@ python -m src.custom_bingo -i data/ChristmasSongsBingo.csv -o output.pdf
# Generate multiple BINGO cards
python -m src.custom_bingo -i data/ChristmasSongsBingo.csv -o output.pdf -n 5
# Generate a BINGO card with custom title and sub-headers
python -m src.custom_bingo -i data/ChristmasSongsBingo.csv -o output.pdf -t "Holiday Music Bingo" -s "Traditional,Upbeat,Holy,Classic,Festive"
```
## Input Format

View File

@@ -12,20 +12,32 @@ from custom_bingo.pdf_generator import export_cards_to_pdf
help='Output PDF file for the BINGO cards')
@click.option('--number-of-cards', '-n', default=1, type=int,
help='Number of BINGO cards to generate (default: 1)')
def main(input_file, output_file, number_of_cards):
@click.option('--title', '-t', default='BINGO', type=str,
help='Title text to display at the top of the card (default: BINGO)')
@click.option('--sub-headers', '-s', default=None, type=str,
help='Comma-separated sub-headers for each column (e.g., "Category1,Category2,Category3,Category4,Category5")')
def main(input_file, output_file, number_of_cards, title, sub_headers):
"""Generate custom BINGO cards from a spreadsheet."""
try:
# Read the spreadsheet data
click.echo(f"Reading data from {input_file}...")
data = read_spreadsheet(input_file)
# Parse sub-headers if provided
sub_headers_list = None
if sub_headers:
sub_headers_list = [h.strip() for h in sub_headers.split(',')]
if len(sub_headers_list) != 5:
click.echo("Error: Sub-headers must contain exactly 5 comma-separated values for the 5 columns.", err=True)
return
# Generate the bingo cards
click.echo(f"Generating {number_of_cards} BINGO card(s)...")
cards = generate_bingo_cards(data, number_of_cards)
# Export to PDF
# Export to PDF with custom title and sub-headers
click.echo(f"Exporting cards to {output_file}...")
export_cards_to_pdf(cards, output_file)
export_cards_to_pdf(cards, output_file, title=title, sub_headers=sub_headers_list)
click.echo(f"Successfully generated {number_of_cards} BINGO card(s) in {output_file}")
except Exception as e:

View File

@@ -94,13 +94,15 @@ def draw_multiline_text(c: canvas.Canvas, text: str, x: float, y: float, width:
c.drawString(centered_x, y_position, line)
def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str):
def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str, title: str = "BINGO", sub_headers: List[str] = None):
"""
Export the generated BINGO cards to a PDF file.
Args:
cards: List of 5x5 matrices representing BINGO cards
output_file: Path for the output PDF file
title: Custom title to display at the top of the card
sub_headers: Optional list of sub-headers for each column (B, I, N, G, O)
"""
# Create a new PDF document
c = canvas.Canvas(output_file, pagesize=letter)
@@ -116,30 +118,41 @@ def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str):
# Starting position for the first card (centered horizontally)
start_x = (width - card_width) / 2
# Position the card lower on the page to accommodate title and headers better
# Increase distance between title and grid
title_height = 0.5 * inch # Space for title
header_height = cell_height # Space for headers
space_between_title_and_headers = 0.3 * inch # Space to prevent overlap
start_y = height - margin - title_height - space_between_title_and_headers - header_height - card_height
# Position the card lower on the page to accommodate title, sub-headers and headers
title_height = 0.5 * inch # Space for main title
sub_header_height = cell_height * 0.5 if sub_headers else 0 # Space for sub-headers if present
header_height = cell_height # Space for column headers
space_between_elements = 0.2 * inch # Space between elements
start_y = height - margin - title_height - space_between_elements - sub_header_height - space_between_elements - header_height - card_height
for idx, card in enumerate(cards):
# Add a new page for each card after the first
if idx > 0:
c.showPage()
start_y = height - margin - title_height - space_between_title_and_headers - header_height - card_height
start_y = height - margin - title_height - space_between_elements - sub_header_height - space_between_elements - header_height - card_height
# Draw title "BINGO"
# Draw custom title
c.setFont("Helvetica", 24)
title_width = c.stringWidth("BINGO", "Helvetica", 24)
title_y = height - margin # Position title at the top with margin
c.drawString((width - title_width) / 2, title_y, "BINGO")
title_width = c.stringWidth(title, "Helvetica", 24)
title_y = height - margin # Position main title at the top with margin
c.drawString((width - title_width) / 2, title_y, title)
# Draw column headers (B, I, N, G, O) below the title with more spacing
# Draw sub-headers if provided
if sub_headers:
for col, sub_header in enumerate(sub_headers):
cell_x = start_x + col * cell_width
# Position sub-headers between the main column headers and the grid
sub_header_y = start_y + card_height + header_height + space_between_elements/2
# Draw sub-header text centered in the sub-header area
draw_multiline_text(c, sub_header, cell_x, sub_header_y, cell_width, sub_header_height, max_font_size=12)
# Draw column headers (B, I, N, G, O) below the sub-headers (or title if no sub-headers)
headers = ['B', 'I', 'N', 'G', 'O']
for col, header in enumerate(headers):
cell_x = start_x + col * cell_width
# Position headers above the grid with adequate spacing from title
# Position headers above the grid with adequate spacing
header_y = start_y + card_height # Headers positioned right above the grid
# Draw header text centered in the header cell

View File

@@ -8,7 +8,7 @@ from custom_bingo.main import main
def test_main_command():
"""Test the main CLI command with a temporary CSV file."""
runner = CliRunner()
# Create a temporary CSV file
csv_content = """B,I,N,G,O
Apple,Book,Car,Door,Elephant
@@ -16,18 +16,18 @@ Banana,Clock,Desk,Engine,Fish
Orange,Pencil,Tree,Flower,Guitar
Grape,Eraser,River,Grass,Hat
Lemon,Notebook,Moon,Leaf,Jacket"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as csv_file:
csv_file.write(csv_content)
csv_path = csv_file.name
# Create a temporary output PDF file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_file:
pdf_path = pdf_file.name
# Remove the PDF file so the command can create it
os.remove(pdf_path)
try:
# Run the CLI command
result = runner.invoke(main, [
@@ -35,15 +35,111 @@ Lemon,Notebook,Moon,Leaf,Jacket"""
'--output-file', pdf_path,
'--number-of-cards', '1'
])
# Check that the command completed successfully
assert result.exit_code == 0
assert "Successfully generated" in result.output
# Check that the output PDF file was created
assert os.path.exists(pdf_path)
assert os.path.getsize(pdf_path) > 0
finally:
# Clean up temporary files
if os.path.exists(csv_path):
os.remove(csv_path)
if os.path.exists(pdf_path):
os.remove(pdf_path)
def test_main_command_with_custom_title():
"""Test the main CLI command with custom title."""
runner = CliRunner()
# Create a temporary CSV file
csv_content = """B,I,N,G,O
Apple,Book,Car,Door,Elephant
Banana,Clock,Desk,Engine,Fish
Orange,Pencil,Tree,Flower,Guitar
Grape,Eraser,River,Grass,Hat
Lemon,Notebook,Moon,Leaf,Jacket"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as csv_file:
csv_file.write(csv_content)
csv_path = csv_file.name
# Create a temporary output PDF file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_file:
pdf_path = pdf_file.name
# Remove the PDF file so the command can create it
os.remove(pdf_path)
try:
# Run the CLI command with custom title
result = runner.invoke(main, [
'--input-file', csv_path,
'--output-file', pdf_path,
'--number-of-cards', '1',
'--title', 'Custom Title'
])
# Check that the command completed successfully
assert result.exit_code == 0
assert "Successfully generated" in result.output
# Check that the output PDF file was created
assert os.path.exists(pdf_path)
assert os.path.getsize(pdf_path) > 0
finally:
# Clean up temporary files
if os.path.exists(csv_path):
os.remove(csv_path)
if os.path.exists(pdf_path):
os.remove(pdf_path)
def test_main_command_with_sub_headers():
"""Test the main CLI command with sub-headers."""
runner = CliRunner()
# Create a temporary CSV file
csv_content = """B,I,N,G,O
Apple,Book,Car,Door,Elephant
Banana,Clock,Desk,Engine,Fish
Orange,Pencil,Tree,Flower,Guitar
Grape,Eraser,River,Grass,Hat
Lemon,Notebook,Moon,Leaf,Jacket"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as csv_file:
csv_file.write(csv_content)
csv_path = csv_file.name
# Create a temporary output PDF file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_file:
pdf_path = pdf_file.name
# Remove the PDF file so the command can create it
os.remove(pdf_path)
try:
# Run the CLI command with sub-headers
result = runner.invoke(main, [
'--input-file', csv_path,
'--output-file', pdf_path,
'--number-of-cards', '1',
'--sub-headers', 'Fruits,Objects,Nouns,Actions,Accessories'
])
# Check that the command completed successfully
assert result.exit_code == 0
assert "Successfully generated" in result.output
# Check that the output PDF file was created
assert os.path.exists(pdf_path)
assert os.path.getsize(pdf_path) > 0
finally:
# Clean up temporary files
if os.path.exists(csv_path):