-
Notifications
You must be signed in to change notification settings - Fork 42
Add certificate automation #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # WCC Certificate Automation | ||
|
|
||
| Automated certificate generation from PowerPoint templates and converting them to PDF format using JSON configuration | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Python 3.7 or higher | ||
| - Microsoft PowerPoint (required for PDF conversion on Windows) | ||
|
|
||
| ## Installation | ||
|
|
||
| Install required Python packages: | ||
| ```bash | ||
| pip install -r requirements.txt | ||
| ``` | ||
|
|
||
|
|
||
| #### Dependencies | ||
|
|
||
| - `python-pptx>=0.6.21`: PowerPoint file manipulation | ||
| - `comtypes>=1.1.14`: COM automation for PowerPoint | ||
|
|
||
|
|
||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| wcc_certificate_automation/ | ||
| ├── README.md # This file | ||
| ├── requirements.txt # Python dependencies | ||
| ├── src/ | ||
| │ ├── config.json # Main configuration file | ||
| │ └── generate_certificates.py # Main automation script | ||
| └── data/ | ||
| ├── input/ | ||
| │ ├── templates/ # PPTX template files | ||
| │ │ ├── mentee.pptx | ||
| │ │ └── mentor.pptx | ||
| │ └── names/ # Names text files | ||
| │ ├── mentees.txt | ||
| │ └── mentors.txt | ||
| └── output/ # Generated certificates | ||
| ├── ppts/ # Generated PPTX files | ||
| │ ├── mentee/ | ||
| │ └── mentor/ | ||
| └── pdfs/ # Generated PDF files | ||
| ├── mentee/ | ||
| └── mentor/ | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| Edit `src/config.json` to customize certificate generation settings. | ||
|
|
||
| ### Config File | ||
|
|
||
| ```json | ||
| { | ||
| "certificate_types": [ | ||
| { | ||
| "type": "mentee", | ||
| "template": "../data/input/templates/mentee.pptx", | ||
| "names_file": "../data/input/names/mentees.txt", | ||
| "pdf_dir": "../data/output/pdfs/mentee/", | ||
| "ppt_dir": "../data/output/ppts/mentee/", | ||
| "placeholder_text": "Sample Sample", | ||
| "font_name": "Georgia", | ||
| "font_size": 59.5 | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| - **type**: Certificate type identifier (e.g., "mentee", "mentor") | ||
| - **template**: Path to the PPTX template file | ||
| - **names_file**: Path to text file containing names (one per line) | ||
| - **pdf_dir**: Output directory for PDF certificates | ||
| - **ppt_dir**: Output directory for PPTX certificates | ||
| - **placeholder_text**: Text in template to be replaced with names | ||
| - **font_name**: Font to use for names | ||
| - **font_size**: Font size in points | ||
|
|
||
| ### Names Files | ||
|
|
||
| Create text files in `data/input/names/` with one name per line: | ||
|
|
||
| **Example** (`data/input/names/mentees.txt`): | ||
| ``` | ||
| John Smith | ||
| Jane Doe | ||
| Alice Johnson | ||
| ``` | ||
|
|
||
| ### Template Files | ||
|
|
||
| Create PPTX template files in `data/input/templates/` with text placeholder: | ||
|
|
||
| **Example** (`data/input/templates/mentee.pptx`): | ||
|
|
||
| ## Usage | ||
|
|
||
| Navigate to the `src` directory and run the main script: | ||
|
|
||
| ```bash | ||
| cd src | ||
| python generate_certificates.py | ||
| ``` | ||
|
|
||
| The script will automatically: | ||
| 1. Check for duplicate names in the input files | ||
| 2. Generate PPTX certificates for each person | ||
| 3. Verify all PPTX certificates were created | ||
| 4. Convert all PPTX certificates to PDF format | ||
| 5. Verify all PDF certificates were created | ||
| 6. Display summary statistics | ||
|
|
||
| ## Output Format | ||
|
|
||
| Generated certificate files are named using the person's name directly: | ||
| - PPTX: `data/output/ppts/mentee/John Smith.pptx` | ||
| - PDF: `data/output/pdfs/mentee/John Smith.pdf` | ||
|
|
||
| ## Sample Logs | ||
|
|
||
| ``` | ||
| Generating MENTEE mentee certificates at ../data/output/ppts/mentee/ | ||
| [1/68] Generated: ../data/output/ppts/mentee/John Smith.pptx | ||
| [2/68] Generated: ../data/output/ppts/mentee/Jane Doe.pptx | ||
| ... | ||
|
|
||
| Successfully generated 68/68 mentee certificates | ||
|
|
||
| Checking metrics MENTEE certificates | ||
|
|
||
| Expected certificates: 68 | ||
| Found certificates: 68 | ||
|
|
||
| All mentee certificates are present! | ||
|
|
||
| Generating MENTEE mentee certificates at ../data/output/pdfs/mentee/ | ||
| [1/68] Generated: ../data/output/pdfs/mentee/John Smith.pdf | ||
| ... | ||
|
|
||
| Type: mentee Total: 68 PPTX Generated: 68 PDF Generated: 68 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| TestFN TestLN | ||
| FirstName LastName | ||
| TestFN TestLN | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| TestFN TestLN | ||
| FirstName LastName |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| python-pptx>=0.6.21 | ||
| comtypes>=1.1.14 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "certificate_types": [ | ||
| { | ||
| "type": "mentee", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great idea! Looking forward to you merge it and I can extend to new certificates :) |
||
| "template": "../data/input/templates/mentee.pptx", | ||
| "names_file": "../data/input/names/mentees.txt", | ||
| "pdf_dir": "../data/output/pdfs/mentee/", | ||
| "ppt_dir": "../data/output/ppts/mentee/", | ||
| "placeholder_text": "Sample Sample", | ||
| "font_name": "Georgia", | ||
| "font_size": 59.5 | ||
| }, | ||
| { | ||
| "type": "mentor", | ||
| "template": "../data/input/templates/mentor.pptx", | ||
| "names_file": "../data/input/names/mentors.txt", | ||
| "pdf_dir": "../data/output/pdfs/mentor/", | ||
| "ppt_dir": "../data/output/ppts/mentor/", | ||
| "placeholder_text": "Sample Sample", | ||
| "font_name": "Georgia", | ||
| "font_size": 59.5 | ||
| } | ||
| ] | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we write some tests too?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 Very important! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| from collections import Counter | ||
| from pathlib import Path | ||
|
|
||
| import comtypes.client | ||
| from pptx import Presentation | ||
| import os | ||
| import json | ||
| import sys | ||
| from pptx.util import Pt | ||
|
|
||
| def load_config(config_path="config.json"): | ||
| with open(config_path, 'r', encoding='utf-8') as f: | ||
| return json.load(f) | ||
|
|
||
| def check_duplicates(names, cert_type): | ||
| counts = Counter(names) | ||
| duplicates = [name for name, count in counts.items() if count > 1] | ||
|
|
||
| if duplicates: | ||
| print(f"\nWARNING: Found {len(duplicates)} duplicate name(s) in {cert_type} list:") | ||
| for name in duplicates: | ||
| print(f" - {name} (appears {counts[name]} times)") | ||
|
|
||
| def load_names(names_file, cert_type): | ||
| all_names = [] | ||
| with open(names_file, 'r', encoding='utf-8') as f: | ||
| all_names += [line.strip() for line in f if line.strip()] | ||
| check_duplicates(all_names, cert_type) | ||
| return set(all_names) | ||
|
|
||
| def generate_certificates_for_type(names, cert_config, file_type): | ||
| template = cert_config['template'] | ||
| input_dir = cert_config["ppt_dir"] if file_type == "pdf" else None | ||
| output_dir = cert_config["ppt_dir"] if file_type == "pptx" else cert_config[ | ||
| 'pdf_dir'] | ||
| placeholder_text = cert_config['placeholder_text'] | ||
| font_name = cert_config['font_name'] | ||
| font_size = cert_config['font_size'] | ||
| cert_type = cert_config['type'] | ||
|
|
||
| os.makedirs(output_dir, exist_ok=True) | ||
|
|
||
| print(f"Generating {cert_type.upper()} {cert_type} certificates at {output_dir}") | ||
|
|
||
| file_count = 0 | ||
|
|
||
| for i, name in enumerate(names, 1): | ||
| file_name = None | ||
| try: | ||
| if file_type == "pptx": | ||
| file_name = generate_pptx(font_name, font_size, name, output_dir, | ||
| placeholder_text, template) | ||
| elif file_type == "pdf": | ||
| file_name = generate_pdf(name, input_dir, output_dir) | ||
|
|
||
| print(f"[{i}/{len(names)}] Generated: {file_name}") | ||
| file_count += 1 | ||
|
|
||
| except Exception as e: | ||
| print(f"[{i}/{len(names)}] ERROR generating {file_type} " | ||
| f"certificate for {name}: {e}") | ||
|
|
||
| print(f"\nSuccessfully generated {file_count}/{len(names)} {cert_type} certificates") | ||
| return file_count | ||
|
|
||
|
|
||
| def generate_pptx(font_name, font_size, name, output_dir, placeholder_text, | ||
| template): | ||
| try: | ||
| prs = Presentation(template) | ||
| for slide in prs.slides: | ||
| for shape in slide.shapes: | ||
| if shape.has_text_frame and shape.text.strip() == placeholder_text: | ||
| tf = shape.text_frame | ||
| tf.clear() | ||
|
|
||
| p = tf.paragraphs[0] | ||
| run = p.add_run() | ||
| run.text = name | ||
| run.font.name = font_name | ||
| run.font.size = Pt(font_size) | ||
| pptx_path = os.path.join(output_dir, f"{name}.pptx") | ||
| prs.save(pptx_path) | ||
| return pptx_path | ||
| except Exception as e: | ||
| raise e | ||
|
|
||
| def generate_pdf(name, input_dir, output_path): | ||
| try: | ||
|
|
||
| output_path = Path(output_path) | ||
|
|
||
| pptx_path = os.path.abspath(os.path.join(input_dir, f"{name}.pptx")) | ||
|
|
||
| if not os.path.exists(pptx_path): | ||
| raise FileNotFoundError(f"PPTX not found: {pptx_path}") | ||
|
|
||
| presentation = powerpoint.Presentations.Open(pptx_path) | ||
|
|
||
| pdf_path = output_path / f"{name}.pdf" | ||
|
|
||
| presentation.SaveAs(str(pdf_path.resolve()), 32) | ||
|
|
||
| presentation.Close() | ||
|
|
||
| return pdf_path | ||
|
|
||
| except Exception as e: | ||
| raise e | ||
|
|
||
| def check_metrics(names, cert_config, file_type): | ||
| cert_type = cert_config['type'] | ||
| output_dir = cert_config["ppt_dir"] if file_type == "pptx" else \ | ||
| cert_config['pdf_dir'] | ||
|
|
||
| print(f"Checking metrics {cert_type.upper()} certificates") | ||
|
|
||
| folder_path = Path(output_dir) | ||
|
|
||
| if not folder_path.exists(): | ||
| print(f"ERROR: Directory does not exist: {output_dir}") | ||
| return 0, len(names) | ||
|
|
||
| existing_files = {f.stem for f in folder_path.glob(f"*.{file_type}")} | ||
|
|
||
| print(f"\nExpected certificates: {len(names)}") | ||
| print(f"Found certificates: {len(existing_files)}") | ||
|
|
||
| missing = [] | ||
| for name in names: | ||
| if name not in existing_files: | ||
| missing.append(name) | ||
|
|
||
| if missing: | ||
| print(f"\nMissing {len(missing)} certificate(s):") | ||
| for name in missing: | ||
| print(f" - {name}") | ||
| else: | ||
| print(f"\nAll {cert_type} certificates are present!") | ||
|
|
||
| def main(): | ||
| global powerpoint | ||
| try: | ||
| config = load_config() | ||
|
|
||
| powerpoint = comtypes.client.CreateObject("PowerPoint.Application") | ||
| powerpoint.Visible = 1 | ||
|
|
||
| for cert_config in config['certificate_types']: | ||
| names_file = cert_config['names_file'] | ||
| cert_type = cert_config['type'] | ||
|
|
||
| names = load_names(names_file, cert_type) | ||
|
|
||
| pptx_generated = generate_certificates_for_type(names, | ||
| cert_config, | ||
| "pptx") | ||
| check_metrics(names, cert_config, "pptx") | ||
| pdf_generated = generate_certificates_for_type(names, | ||
| cert_config, "pdf") | ||
| check_metrics(names, cert_config, "pdf") | ||
| total_certificates = len(names) | ||
| print(f"Type: {cert_config['type']} Total: {total_certificates} " | ||
| f"PPTX Generated: {pptx_generated} PDF Generated: {pdf_generated}") | ||
|
|
||
|
|
||
| except Exception as e: | ||
| print(f"Error while running the certificate generation automation:" | ||
| f" {e}") | ||
| return 1 | ||
| finally: | ||
| if powerpoint: | ||
| powerpoint.Quit() | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please move this folder under tools and update README there to include this extra automation. Thanks for the amazing script, I will give a try locally also, to extend and create another type of certificate.