diff --git a/badges_app/Dockerfile b/badges_app/Dockerfile new file mode 100644 index 0000000..a1307be --- /dev/null +++ b/badges_app/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu:18.04 + +RUN apt update && \ + apt install -y wget bash python3.6-venv python3.6-dev python3-pip build-essential inkscape unzip librsvg2-bin poppler-utils + +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +WORKDIR /app + +COPY requirements.txt . +RUN pip3 install --upgrade pip && pip install -r requirements.txt + +COPY . . + +RUN wget --header 'Host: dl.dafont.com' --header 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' --header 'Accept-Language: en-US,en;q=0.5' --referer 'https://www.dafont.com/sansation.font' --header 'Upgrade-Insecure-Requests: 1' 'https://dl.dafont.com/dl/?f=sansation' --output-document 'sansation.zip' && \ + unzip sansation.zip -d /usr/local/share/fonts && \ + fc-cache -f -v + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/badges_app/README.md b/badges_app/README.md new file mode 100644 index 0000000..afad6fe --- /dev/null +++ b/badges_app/README.md @@ -0,0 +1,33 @@ +# Conference Badge Generator WebApp + +A web application for generating conference badges from SVG templates and participant CSV data. + +## Features + +- Upload multiple SVG badge templates +- Upload multiple CSV files with attendee data +- Two output options: + - Separate PDF files for each badge + - Single merged PDF with 4 badges per page (with customizable dimensions) + +## 🚀 Quick Start with Docker + +1. Navigate to the project directory: + +```bash +cd badges_app +``` + +2. Build the Docker image: + +```bash +docker build -t badge-generator . +``` + +3. Run the container: + +```bash +docker run -d -p 8000:8000 --name badge-app badge-generator +``` + +4. Open the app in your browser: http://localhost:8000 diff --git a/badges_app/__init__.py b/badges_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/badges_app/analyze_csv_and_svg.py b/badges_app/analyze_csv_and_svg.py new file mode 100644 index 0000000..e0119cd --- /dev/null +++ b/badges_app/analyze_csv_and_svg.py @@ -0,0 +1,225 @@ +import argparse +from difflib import SequenceMatcher +import base64 +import io +from fastapi import UploadFile +import xml.etree.ElementTree as ET +import re +from typing import List +from pdf2image import convert_from_bytes +import os +import shutil +import csv + +from generate_badges import create_badges + +# Known naming variants for each target field +KNOWN_VARIANTS = { + 'First Name': ['firstname', 'fname', 'givenname'], + 'Last Name': ['lastname', 'surname', 'lname', 'familyname'], + 'Company Name': ['companyname', 'company name', 'company', 'organization', 'employer'], +} + + +def normalize(name: str) -> str: + """Normalize column name: remove special characters and lowercase it.""" + return re.sub(r'[\s_\-\.]', '', name).lower() + + +def best_match(variants, normalized_columns, used_columns): + """Return the best match from normalized_columns for the given variants.""" + best_score = 0 + best_match_column = None + + for variant in variants: + for column in normalized_columns: + if column in used_columns: + continue + score = SequenceMatcher(None, variant, column).ratio() + if score > best_score: + best_score = score + best_match_column = column + + return best_match_column if best_score >= 0.6 else None + + +def find_matching_columns(target, normalized_columns, used_columns, results): + """Find best matching columns from a CSV file or a list of columns for the given target fields.""" + variants = KNOWN_VARIANTS.get(target, [target]) + match_column = best_match(variants, normalized_columns, used_columns) + + if match_column: + matched_column = normalized_columns[match_column] + used_columns.add(match_column) + + results[target] = { + 'csv_column': matched_column, + 'samples': [] + } + return matched_column + return "" + + +def find_matching_columns_from_list(columns: list, target_fields: list) -> dict: + """Find best matching columns from a columns list for the given target fields.""" + normalized_columns = {normalize(c): c for c in columns} + results = {} + used_columns = set() + + for target in target_fields: + matched_column = find_matching_columns(target, normalized_columns, used_columns, results) + if not matched_column: + results[target] = None + + return results + + +def find_matching_columns_from_csv(csv_file: str, target_fields: list) -> dict: + """Find best matching columns from a CSV file for the given target fields.""" + with open(csv_file, mode='r', encoding='utf-8') as file: + reader = csv.reader(file) + columns = next(reader) + + normalized_columns = {normalize(c): c for c in columns} + results = {} + used_columns = set() + + for target in target_fields: + matched_column = find_matching_columns(target, normalized_columns, used_columns, results) + if matched_column: + # Collect up to 3 non-empty sample values + file.seek(0) + next(reader) # Skip header again + sample_count = 0 + for row in reader: + idx = columns.index(matched_column) + if idx >= len(row): + continue + value = row[idx].strip() + if value: + results[target]['samples'].append(value) + sample_count += 1 + if sample_count >= 3: + break + else: + results[target] = None # No match found + + return results + + +def print_analysis(results: dict): + """Print the matching results in a readable format.""" + print("\nCSV Column Matching Results") + print("=" * 50) + + for target, data in results.items(): + print(f"\nTarget field: '{target}'") + if data: + print(f" Matched column: '{data['csv_column']}'") + if data['samples']: + print(f" Sample values: {', '.join(data['samples'])}") + else: + print(" (No sample values found)") + else: + print(" No matching column found.") + + print("\n" + "=" * 50) + + +def list_of_strings(arg): + return arg.split(',') + + +def svg_to_image(tmp_preview_dir, role, template_filename: str, template_vars) -> str: + create_badges(template_filename, os.path.join(tmp_preview_dir, "preview_data.csv"), tmp_preview_dir, template_vars) + + with open(os.path.join(tmp_preview_dir, f"{role}_0.pdf"), "rb") as f: + images = convert_from_bytes(f.read(), first_page=1, last_page=1) + + buffered = io.BytesIO() + images[0].save(buffered, format="JPEG", quality=85) + return base64.b64encode(buffered.getvalue()).decode() + + +def prepare_preview_data(tmp_preview_dir, template_vars): + preview_data = { + "First Name": "Klaus", + "Last Name": "Templatemann", + "Company Name": "Badgeify" + } + matches = find_matching_columns_from_list(template_vars, preview_data.keys()) + + column_value_map = {} + for target_key, data in matches.items(): + if data: + column_name = data["csv_column"] + column_value_map[column_name] = preview_data.get(target_key, "") + + row = [column_value_map.get(col, "") for col in template_vars] + + with open(os.path.join(tmp_preview_dir, "preview_data.csv"), "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(template_vars or [","]) + writer.writerow(row or [","]) + + +def analyze_svg_templates(svg_files: List[UploadFile], templates_dir: str): + tmp_preview_dir = "tmp_preview" + os.makedirs(tmp_preview_dir, exist_ok=True) + pattern = re.compile(r"\{\{([^}]+)\}\}") + results = {} + previews = {} + + try: + for svg_file in svg_files: + template_filename = os.path.join(templates_dir, svg_file.filename) + with open(os.path.join(templates_dir, svg_file.filename), "rb") as f: + try: + root = ET.fromstring(f.read()) + template_vars = set() + for elem in root.iter(): + if elem.text and pattern.search(elem.text): + template_vars.update(pattern.findall(elem.text)) + for attr in elem.attrib.values(): + if pattern.search(attr): + template_vars.update(pattern.findall(attr)) + + results[svg_file.filename] = sorted(template_vars) + prepare_preview_data(tmp_preview_dir, template_vars) + previews[svg_file.filename] = svg_to_image(tmp_preview_dir, svg_file.filename.split('.')[0], template_filename, template_vars) + except ET.ParseError as e: + print(f"Error parsing SVG {svg_file.filename}: {e}") + results[svg_file.filename] = ["Invalid SVG file"] + previews[svg_file.filename] = None + + return results, previews + finally: + shutil.rmtree(tmp_preview_dir, ignore_errors=True) + + +if __name__ == "__main__": + TARGET_FIELDS = ['First Name', 'Last Name', 'Company Name'] + + parser = argparse.ArgumentParser( + description="Find matching columns in a CSV file." + ) + parser.add_argument( + "--csv_file", + help="Path to the CSV file to analyze", + type=str, + default=None + ) + parser.add_argument( + "--columns", + help="List of columns to analyze", + type=list_of_strings, + default=None + ) + args = parser.parse_args() + + matches = {} + if args.csv_file: + matches = find_matching_columns_from_csv(args.csv_file, TARGET_FIELDS) + elif args.columns: + matches = find_matching_columns_from_list(args.columns, TARGET_FIELDS) + print_analysis(matches) diff --git a/badges_app/generate_badges.py b/badges_app/generate_badges.py new file mode 100644 index 0000000..182b509 --- /dev/null +++ b/badges_app/generate_badges.py @@ -0,0 +1,52 @@ +import csv +import json +import os +import shutil +import subprocess + + +class MissingCSVFieldsException(Exception): + def __init__(self, svg_filename, csv_filename, missing_fields): + message = f""" + We’re unable to proceed with badge generation.

+ Details:
+ The following fields are used in the badge template {os.path.basename(svg_filename)} but they are missing in the uploaded CSV file {os.path.basename(csv_filename)}:
+ +
+ Please check your CSV file and ensure all required fields are present + """ + super().__init__(message) + + +def check_svg_and_csv_consistency(svg_file, csv_file, svg_analysis): + with open(csv_file, mode='r', encoding='utf-8') as file: + reader = csv.reader(file) + columns = next(reader) + + missing_fields = [field for field in svg_analysis if field not in columns] + if missing_fields: + raise MissingCSVFieldsException(svg_file, csv_file, missing_fields) + + +def create_badges(template_file, input_file, output_dir, svg_analysis): + check_svg_and_csv_consistency(template_file, input_file, svg_analysis) + + cmd = f'docstamp create -i {input_file} -t {template_file} -d pdf -o {output_dir} --index ""' + print('Calling {}'.format(cmd)) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + _stdout, stderr = process.communicate() + if stderr and b'Failed to get connection' not in stderr: + raise Exception(stderr.decode()) + + +def create_all_badges(roles, templates_dir, data_dir, output_dir, svg_analysis_results): + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + os.makedirs(output_dir) + for role in roles: + create_badges( + os.path.join(templates_dir, f"{role}.svg"), os.path.join(data_dir, f"{role}.csv"), output_dir, + json.loads(svg_analysis_results)[f"{role}.svg"] + ) diff --git a/badges_app/main.py b/badges_app/main.py new file mode 100644 index 0000000..9bf9a3a --- /dev/null +++ b/badges_app/main.py @@ -0,0 +1,200 @@ +import io +import os +import shutil +import zipfile +from typing import List + +from fastapi import FastAPI, File, Form, Request, UploadFile +from fastapi.responses import HTMLResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from generate_badges import create_all_badges, MissingCSVFieldsException +from merge_badges_into_pdf import BadgeMerger +from analyze_csv_and_svg import analyze_svg_templates + + +app = FastAPI(title="Badge Generator") +app.mount("/static", StaticFiles(directory="static"), name="static") +TEMPLATES = Jinja2Templates(directory="templates") +ERROR_MESSAGE = ( + "We’re unable to proceed with badge generation.

" + "Suggested steps:
" + "• Verify your input files for accuracy
" + "• Ensure all required data is provided
" + "• Try the process again" +) +UPLOAD_TEMPLATES_DIR = "uploaded_templates" +UPLOAD_DATA_DIR = "uploaded_data" +GENERATED_DIR = "generated_badges" + + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + return TEMPLATES.TemplateResponse( + "index.html", + {"request": request, "app_name": "Conference Badge Manager"} + ) + + +@app.post("/analyze-svg") +async def analyze_svg( + request: Request, + svg_files: List[UploadFile] = File(...) +): + try: + save_uploaded_files(svg_files, UPLOAD_TEMPLATES_DIR) + results, previews = analyze_svg_templates(svg_files, UPLOAD_TEMPLATES_DIR) + return TEMPLATES.TemplateResponse( + "index.html", + {"request": request, "results": results, "app_name": "SVG Analysis", "previews": previews} + ) + except Exception as e: + print("Error: %s" % str(e)) + return render_error_page( + request, + "Failed to analyze SVG files", + "We’re unable to proceed with SVG Analysis" + ) + + +@app.post("/generate-badges") +async def generate_badges( + request: Request, + badge_width: float = Form(...), + badge_height: float = Form(...), + output_format: str = Form(...), + data_files: List[UploadFile] = File(...), + svg_analysis_results: str = Form(...), +): + try: + roles, error_response = validate_templates_and_data_files(request, data_files) + if error_response: + return error_response + + save_uploaded_files(data_files, UPLOAD_DATA_DIR) + create_all_badges(roles, UPLOAD_TEMPLATES_DIR, UPLOAD_DATA_DIR, GENERATED_DIR, svg_analysis_results.replace("'", '"')) + + if output_format == "merged": + return return_merged_badges(request, badge_width, badge_height) + else: + return return_separate_badges(request) + except MissingCSVFieldsException as e: + return render_error_page( + request, + "Badge Generation Failed", + str(e) + ) + except Exception as e: + print("Error: %s" % str(e)) + return render_error_page( + request, + "Badge Generation Failed", + ERROR_MESSAGE + ) + finally: + shutil.rmtree(UPLOAD_TEMPLATES_DIR, ignore_errors=True) + shutil.rmtree(UPLOAD_DATA_DIR, ignore_errors=True) + + +@app.get("/download/{filename}") +async def download_file(filename: str): + try: + return FileResponse( + filename, + media_type="application/pdf", + filename=filename + ) + finally: + shutil.rmtree(GENERATED_DIR, ignore_errors=True) + + +def render_error_page(request: Request, error_title: str, error_details: str): + return TEMPLATES.TemplateResponse( + "error.html", + { + "request": request, + "app_name": "Conference Badge Manager", + "error_title": error_title, + "error_details": error_details + } + ) + + +def save_uploaded_files(files, directory): + if os.path.exists(directory): + shutil.rmtree(directory) + os.makedirs(directory) + + for template in files: + path = os.path.join(directory, template.filename) + with open(path, "wb") as buffer: + shutil.copyfileobj(template.file, buffer) + + +def validate_templates_and_data_files(request, data_files): + template_files = os.listdir(UPLOAD_TEMPLATES_DIR) if os.path.exists(UPLOAD_TEMPLATES_DIR) else [] + if not template_files or not data_files: + return [], render_error_page( + request, + "Missing Files", + "Both template and data files are required" + ) + + # Check for matching pairs + template_names = {f.split('.')[0] for f in template_files} + data_names = {f.filename.split('.')[0] for f in data_files} + + missing_templates = data_names - template_names + missing_data = template_names - data_names + + if missing_templates: + return [], render_error_page( + request, + "Missing Templates", + f"Missing template files for these roles: {', '.join(missing_templates)}" + ) + + if missing_data: + return [], render_error_page( + request, + "Missing Data Files", + f"Missing data files for these roles: {', '.join(missing_data)}" + ) + return data_names, None + + +def build_success_response(request: Request, filename: str): + return TEMPLATES.TemplateResponse( + "index.html", + { + "request": request, + "download_url": f"/download/{filename}", + "success": True, + "app_name": "Conference Badge Manager" + } + ) + +def return_merged_badges(request, badge_width, badge_height): + merger = BadgeMerger(badge_width_cm=badge_width, badge_height_cm=badge_height) + output_pdf = "merged_badges.pdf" + merger.merge_badges(GENERATED_DIR, output_pdf) + return build_success_response(request, output_pdf) + + +def pack_badges_into_zip(output_dir): + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: + files = os.listdir(output_dir) + for file_path in files: + zip_file.write(os.path.join(output_dir, file_path), file_path) + zip_buffer.seek(0) + return zip_buffer + + +def return_separate_badges(request): + zip_buffer = pack_badges_into_zip(GENERATED_DIR) + zip_filename = "generated_badges.zip" + with open(zip_filename, "wb") as f: + f.write(zip_buffer.getvalue()) + return build_success_response(request, zip_filename) diff --git a/badges_app/merge_badges_into_pdf.py b/badges_app/merge_badges_into_pdf.py new file mode 100644 index 0000000..4889f09 --- /dev/null +++ b/badges_app/merge_badges_into_pdf.py @@ -0,0 +1,103 @@ +from PyPDF2 import PdfWriter, PdfReader +from PyPDF2.generic._rectangle import RectangleObject +import os +from io import BytesIO +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.lib.colors import black + + +class BadgeMerger: + def __init__(self, badge_width_cm=7.5, badge_height_cm=12): + self.badge_width = badge_width_cm * 28.35 # Convert cm to points + self.badge_height = badge_height_cm * 28.35 + self.a4_center = (A4[0] / 2, A4[1] / 2) + self.positions = { + 1: { + 1: (1, 0, 0, 1, self.a4_center[0] - self.badge_width, self.a4_center[1]), + 2: (1, 0, 0, 1, self.a4_center[0], self.a4_center[1]), + 3: (1, 0, 0, 1, self.a4_center[0] - self.badge_width, self.a4_center[1] - self.badge_height), + 4: (1, 0, 0, 1, self.a4_center[0], self.a4_center[1] - self.badge_height) + }, + 2: { + 1: (1, 0, 0, 1, self.a4_center[0], self.a4_center[1]), + 2: (1, 0, 0, 1, self.a4_center[0] - self.badge_width, self.a4_center[1]), + 3: (1, 0, 0, 1, self.a4_center[0], self.a4_center[1] - self.badge_height), + 4: (1, 0, 0, 1, self.a4_center[0] - self.badge_width, self.a4_center[1] - self.badge_height), + } + } + + def _draw_guide_lines(self): + """Create a PDF page with cutting guide lines""" + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=A4) + can.setStrokeColor(black) + can.setLineWidth(0.001) + can.lines([ + (self.a4_center[0] - self.badge_width, self.a4_center[1] + self.badge_height, + self.a4_center[0] - self.badge_width, self.a4_center[1] - self.badge_height), + (self.a4_center[0] - self.badge_width, self.a4_center[1] + self.badge_height, + self.a4_center[0] + self.badge_width, self.a4_center[1] + self.badge_height), + (self.a4_center[0] - self.badge_width, self.a4_center[1] - self.badge_height, + self.a4_center[0] + self.badge_width, self.a4_center[1] - self.badge_height), + (self.a4_center[0] + self.badge_width, self.a4_center[1] + self.badge_height, + self.a4_center[0] + self.badge_width, self.a4_center[1] - self.badge_height), + (self.a4_center[0], self.a4_center[1] - self.badge_height, + self.a4_center[0], self.a4_center[1] + self.badge_height) + ]) + can.save() + packet.seek(0) + return PdfReader(packet).pages[0] + + def _create_page_template(self): + """Create a blank PDF page template""" + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=A4) + can.drawString(0, 0, "") + can.showPage() + can.drawString(0, 0, "") + can.save() + packet.seek(0) + return PdfReader(packet).pages[0], PdfReader(packet).pages[1] + + def _arrange_badges_on_page(self, badges_folder, badges, page_number): + """Arrange 4 badges on a single page""" + page_template, _ = self._create_page_template() + + for i, badge in enumerate(badges, 1): + badge_path = os.path.join(badges_folder, badge) + badge_reader = PdfReader(badge_path) + badge_page = badge_reader.pages[0] + + badge_page.scale_to(self.badge_width, self.badge_height) + badge_page.trimbox = RectangleObject([0.0, 0.0, A4[0], A4[1]]) + badge_page.add_transformation(self.positions[page_number][i]) + page_template.merge_page(badge_page) + + guide_lines = self._draw_guide_lines() + page_template.merge_page(guide_lines) + + return page_template + + def merge_badges(self, badges_folder, output_file): + """Merge all badges from folder into a single PDF with 4 badges per page""" + pdf_writer = PdfWriter() + all_badges = sorted([f for f in os.listdir(badges_folder) if f.endswith('.pdf')]) + + for i in range(0, len(all_badges), 4): + badges_batch = all_badges[i:i + 4] + + # Create two pages (front and back) for each 4 badges + front_page = self._arrange_badges_on_page(badges_folder, badges_batch, 1) + back_page = self._arrange_badges_on_page(badges_folder, badges_batch, 2) + + pdf_writer.add_page(front_page) + pdf_writer.add_page(back_page) + + with open(output_file, "wb") as f: + pdf_writer.write(f) + + +if __name__ == "__main__": + merger = BadgeMerger(badge_width_cm=7.5, badge_height_cm=12) + merger.merge_badges("stamped", "final_badges.pdf") diff --git a/badges_app/requirements.txt b/badges_app/requirements.txt new file mode 100644 index 0000000..7ff872d --- /dev/null +++ b/badges_app/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.83.0 +uvicorn==0.16.0 +jinja2==3.0.3 +docstamp==0.4.5 +python-multipart==0.0.5 +reportlab==3.6.8 +pdf2image==1.17.0 diff --git a/badges_app/static/styles.css b/badges_app/static/styles.css new file mode 100644 index 0000000..8b7428e --- /dev/null +++ b/badges_app/static/styles.css @@ -0,0 +1,228 @@ +body { + background-color: #f8f9fa; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.main-container { + max-width: 800px; + margin-top: 3rem; + margin-bottom: 3rem; +} + +.header { + color: #2c3e50; +} + +.header h1 { + font-weight: 700; + color: #2c3e50; +} + +.upload-form { + background-color: white; + padding: 2rem; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); +} + +.card { + border: none; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; +} + +.card-header { + border-radius: 8px 8px 0 0 !important; + padding: 1rem 1.5rem; +} + +.card-header.bg-info { + background-color: #0dcaf0 !important; +} + +.card-header.bg-danger { + background-color: #dc3545 !important; +} + +.generate-btn { + padding: 0.75rem 2rem; + font-weight: 600; + min-width: 200px; +} + +.form-control { + padding: 0.75rem 1rem; + border: 1px solid #ced4da; + border-radius: 6px; +} + +.form-control:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.form-text { + color: #6c757d; + font-size: 0.85rem; +} + +.status-alert { + transition: all 0.3s ease; +} + +.alert-warning { + background-color: #fff3cd; + border-color: #ffeeba; + color: #856404; +} + +.alert-danger { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.text-danger { + color: #dc3545; +} + +.form-check { + margin-bottom: 1rem; + padding-left: 2.5em; +} + +.form-check-input { + width: 1.5em; + height: 1.5em; + margin-top: 0.1em; + margin-left: -2.5em; +} + +.form-check-label { + font-size: 1.1rem; + padding-top: 0.2em; +} + +.form-switch .form-check-input { + width: 3em; +} + +#dimensions-section { + background-color: #f8f9fa; + border-radius: 8px; + margin-top: 1rem; + transition: all 0.3s ease; +} + +.fw-bold { + font-weight: 600 !important; +} + +.fs-5 { + font-size: 1.25rem !important; +} + +pre, .code-example { + font-family: 'Courier New', Courier, monospace; + background-color: #f8f9fa; + border-radius: 4px; + font-size: 0.9rem; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; + padding: 15px; +} + +.code-example { + padding: 2px 4px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner-border { + display: inline-block; + width: 1rem; + height: 1rem; + vertical-align: text-bottom; + border: 0.15em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +@media (max-width: 576px) { + .main-container { + padding: 0 1rem; + } + .header h1 { + font-size: 2rem; + } + .form-check-input { + width: 2em; + height: 1.25em; + } +} +body { + background-color: #f8f9fa; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.main-container { + max-width: 1000px; + margin-top: 3rem; + margin-bottom: 3rem; +} + +.header { + color: #2c3e50; +} + +.header h1 { + font-weight: 700; + color: #2c3e50; +} + +.card { + border: none; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; + height: 100%; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.card-header { + border-radius: 10px 10px 0 0 !important; + padding: 1.25rem 1.5rem; +} + +.card-body { + padding: 1.5rem; +} + +.btn { + padding: 0.75rem 1.5rem; + font-weight: 600; + border-radius: 8px; +} + +@media (max-width: 768px) { + .main-container { + padding: 0 1rem; + } + .header h1 { + font-size: 2.2rem; + } + .card { + margin-bottom: 1.5rem; + } +} \ No newline at end of file diff --git a/badges_app/templates/base.html b/badges_app/templates/base.html new file mode 100644 index 0000000..51ac700 --- /dev/null +++ b/badges_app/templates/base.html @@ -0,0 +1,17 @@ + + + + + + {% block title %}{{ app_name }}{% endblock %} + + + + +
+ {% block content %} + {% endblock %} +
+ + + diff --git a/badges_app/templates/error.html b/badges_app/templates/error.html new file mode 100644 index 0000000..80ae6ef --- /dev/null +++ b/badges_app/templates/error.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}{{ app_name }}{% endblock %} + +{% block content %} +
+
+

{{ app_name }}

+
+ +
+
+
{{ error_title }}
+
+
+
+

{{ error_details | safe }}

+
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/badges_app/templates/index.html b/badges_app/templates/index.html new file mode 100644 index 0000000..d2edf96 --- /dev/null +++ b/badges_app/templates/index.html @@ -0,0 +1,163 @@ +{% extends "base.html" %} + +{% block title %}{{ app_name }} - Badge Generator{% endblock %} + +{% block content %} +
+
+

{{ app_name }}

+
+ + +
+
+
Step 1: Upload SVG Templates & Analyze them
+
+
+
+
+ + +
+
+ +
+
+
+
+ + {% if results %} + +
+

SVG Analysis Results & Badge Preview

+

Below is a summary of required CSV columns based on each uploaded SVG template with badge preview.

+ {% for filename, variables in results.items() %} +
+
+ Template: {{ filename }} +
+
+
+
+ {% if variables[0] == "Invalid SVG file" %} +
Invalid SVG file.
+ {% elif variables|length == 0 %} +
No variables found.
+ {% else %} +

Required CSV columns:

+
    + {% for var in variables %} +
  • {{ var }}
  • + {% endfor %} +
+ {% endif %} +
+
+ Preview +
+
+
+
+ {% endfor %} +
+ {% endif %} + + +
+
+
Step 2: Upload Attendee Data & Generate Badges
+
+
+
+ + + +
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
Badge Dimensions (cm)
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
+ {% if success %} +
+ Badges generated successfully! +
+ Download +
+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/docstamp/cli/cli.py b/docstamp/cli/cli.py index 7c6eee9..db9154c 100644 --- a/docstamp/cli/cli.py +++ b/docstamp/cli/cli.py @@ -50,7 +50,7 @@ def cli(): default='inkscape', show_default=True, help='The rendering command to be used in case file name ' 'extension is not specific.') -@click.option('--index', default=[], +@click.option('--index', type=int, multiple=True, help='Index/es of the CSV file that you want to create the ' 'document from. Note that the samples numbers start from 0 ' 'and the empty ones do not count.') @@ -98,7 +98,7 @@ def create(input, template, field, outdir, prefix, otype, command, index, # filter the items if index if index: - myitems = {int(idx): items[int(idx)] for idx in index} + myitems = {idx: items[idx] for idx in index} items = myitems log.debug('Using the elements with index {} of the input ' 'file.'.format(index))