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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user