diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab18773 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +__pycache__ +build/ +learner_certificates.egg-info/ \ No newline at end of file diff --git a/README.md b/README.md index 678f0a6..3716eb6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,22 @@ # Certificates for The Carpentries +## Using the python program -There are two ways to build certificates from this repo, one depends on the python package cairosvg which in turn depends on cairo development libraries being installed. To use this method, use `bin/certificates.py` to build certificates. +The python program makes use of the `cairosvg` package and supports reading a csv file containing participants using the `pandas` package. You can either install the python package or run it directly from this repo. + +``` +python3 -m learner_certificates.certificates -h +``` + +## Using jinja2 directly The second, pure python method uses the python packages jinja2, jinja2-cli and svglib to build the certificates. To build certificates this way, you can run: ``` -jinja2 swc-attendance.svg -D name="Firstname Lastname" -D date="Nov. 6, 2017" -D instructor="Some Instructor Name" > lastname_firstname.svg +jinja2 learner_certificates/templates/swc-attendance.svg \ + -D name="Firstname Lastname" -D date="Nov. 6, 2017" \ + -D instructor="Some Instructor Name" > lastname_firstname.svg svg2pdf lastname_firstname.svg ``` diff --git a/bin/certificates.py b/bin/certificates.py deleted file mode 100755 index f333386..0000000 --- a/bin/certificates.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 - -''' -Generate a certificate from a template. - -* Requires the python package 'cairosvg' to be installed. - Please visit http://cairosvg.org/ for install instructions. -* Some systems may also need to have 'cairo' installed. - Please visit http://cairographics.org/download/ for the same. - -On a Mac, a typical command line is - -python bin/certificates.py \ - -b swc-instructor - -r $HOME/sc/certification/ \ - -u turing_alan - date='January 24, 1924' \ - instructor='Ada Lovelace' \ - name='Alan Turing' - -where: - - -b: BADGE_TYPE - -r: ROOT_DIRECTORY - -u: USER_ID - name=value: fill-ins for the template - -The script then looks for $(ROOT_DIRECTORY)/$(BADGE_TYPE).svg as a template -file, and creates $(ROOT_DIRECTORY)/$(BADGE_TYPE)/$(USER_ID).pdf as output. - -This script will also take a CSV file as input. The file must contain rows of: - - badge,trainer,user_id,new_instructor,email,date - -such as: - - swc-instructor,Grace Hopper,turing_alan,Alan Turing,alan@turing.org,2016-01-27 - -In this case, the command line invocation is: - -python bin/certificates.py \ - -r $HOME/sc/certification/ \ - -c input.csv -''' - -import sys -import os -import re -import csv -import tempfile -import subprocess -import unicodedata -from optparse import OptionParser -import time -from datetime import date -import cairosvg - - -DATE_FORMAT = '%B %-d, %Y' - - -def main(): - args = parse_args() - if args.csv_file: - process_csv(args) - else: - process_single(args) - - -def parse_args(): - '''Get command-line arguments.''' - - parser = OptionParser() - # -i argument retained for backward incompatibility, - # not used anymore - parser.add_option('-i', '--ink', - default='/Applications/Inkscape.app/Contents/Resources/bin/inkscape', - dest='inkscape', - help='[deprecated] Path to Inkscape') - parser.add_option('-b', '--badge', - default=None, dest='badge_type', help='Type of badge') - parser.add_option('-r', '--root', - default=os.getcwd(), dest='root_dir', help='Root directory (current by default)') - parser.add_option('-u', '--userid', - default=None, dest='user_id', help='User ID') - parser.add_option('-c', '--csv', - default=None, dest='csv_file', help='CSV file') - - args, extras = parser.parse_args() - check(args.root_dir is not None, 'Must specify root directory') - msg = 'Must specify either CSV file or both root directory and user ID' - if args.csv_file is not None: - check((args.badge_type is None) and (args.user_id is None), msg) - elif args.badge_type and args.user_id: - check(args.csv_file is None, msg) - else: - check(False, msg) - - args.params = extract_parameters(extras) - if 'date' not in args.params: - args.params['date'] = date.strftime(date.today(), DATE_FORMAT) - - return args - - -def extract_parameters(args): - '''Extract key-value pairs from command line (checking for uniqueness).''' - - result = {} - for a in args: - fields = a.split('=') - assert len(fields) == 2, 'Badly formatted key-value pair "{0}"'.format(a) - key, value = fields - assert key not in result, 'Duplicate key "{0}"'.format(key) - result[key] = value - return result - - -def process_csv(args): - '''Process a CSV file.''' - - with open(args.csv_file, 'r') as raw: - reader = csv.reader(raw) - for row in reader: - check(len(row) == 6, 'Badly-formatted row in CSV: {0}'.format(row)) - badge_type, args.params['instructor'], user_id, args.params['name'], email, args.params['date'] = row - if '-' in args.params['date']: - d = time.strptime(args.params['date'], '%Y-%m-%d') - d = date(*d[:3]) - args.params['date'] = date.strftime(d, DATE_FORMAT) - template_path = construct_template_path(args.root_dir, badge_type) - output_path = construct_output_path(args.root_dir, badge_type, user_id) - create_certificate(template_path, output_path, args.params) - -def process_single(args): - '''Process a single entry.''' - - template_path = construct_template_path(args.root_dir, args.badge_type) - output_path = construct_output_path(args.root_dir, args.badge_type, args.user_id) - create_certificate(template_path, output_path, args.params) - - -def construct_template_path(root_dir, badge_type): - '''Create path for template file.''' - - return os.path.join(root_dir, badge_type + '.svg') - - -def construct_output_path(root_dir, badge_type, user_id): - '''Create path for generated PDF certificate.''' - - badge_path = os.path.join(root_dir, badge_type) - path_exists = os.path.exists(badge_path) - - if not path_exists: - os.mkdir(badge_path) - - return os.path.join(badge_path, user_id + '.pdf') - - -def create_certificate(template_path, output_path, params): - '''Create a single certificate.''' - - with open(template_path, 'r') as reader: - template = reader.read() - check_template(template, params) - - for key, value in params.items(): - pattern = '{{' + key + '}}' - template = template.replace(pattern, value) - - tmp = tempfile.NamedTemporaryFile(suffix='.svg', delete=False) - tmp.write(bytes(template, 'utf-8')) - - cairosvg.svg2pdf(url=tmp.name, write_to=output_path, dpi=90) - - -def check_template(template, params): - '''Check that all values required by template are present.''' - - expected = re.findall(r'\{\{([^}]*)\}\}', template) - missing = set(expected) - set(params.keys()) - check(not missing, - 'Missing parameters required by template: {0}'.format(' '.join(missing))) - - -def check(condition, message): - '''Fail if condition not met.''' - - if not condition: - print(message, file=sys.stderr) - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/bin/makecert.sh b/bin/makecert.sh deleted file mode 100755 index a441567..0000000 --- a/bin/makecert.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -if [[ $# -eq 0 ]]; -then - echo "makecert.sh " -fi - -# Check for jinja2 -command -v jinja2 >/dev/null 2>&1 || { echo >&2 "This scripts requires jinja2-cli but it's not installed. Install with `pip install jinja2-cli`. Aborting."; exit 1; } -# Check for svg2pdf -command -v svg2pdf>/dev/null 2>&1 || { echo >&2 "This script requires svg2pdf, a part of svglib. Install with `pip install svgilib`. Aborting."; exit 1; } - -source_svg=${1} -context=${2} -certout_pdf=${3} -certout_svg=${certout_pdf/.pdf/.svg} - -jinja2 ${source_svg} ${context} > ${certout_svg} # fill template - -svg2pdf ${certout_svg} # convert to PDF - - diff --git a/learner_certificates/__init__.py b/learner_certificates/__init__.py new file mode 100644 index 0000000..7686a1d --- /dev/null +++ b/learner_certificates/__init__.py @@ -0,0 +1 @@ +from .__version__ import __version__ # noqa F401 diff --git a/learner_certificates/__version__.py b/learner_certificates/__version__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/learner_certificates/__version__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/learner_certificates/certificates.py b/learner_certificates/certificates.py new file mode 100755 index 0000000..a33d79a --- /dev/null +++ b/learner_certificates/certificates.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +''' +Generate a certificate from a template. + +* Requires the python package 'cairosvg' to be installed. + Please visit http://cairosvg.org/ for install instructions. +* Some systems may also need to have 'cairo' installed. + Please visit http://cairographics.org/download/ for the same. + +On a Mac, a typical command line is + +python -m learner_certificates.certificates \ + -b swc-instructor + -o $HOME/sc/certification/ \ + -u turing_alan + -d 'January 24, 1924' \ + -i 'Ada Lovelace' \ + -n 'Alan Turing' + +where: + + -b: BADGE_TYPE + -o: the OUTPUT_DIRECTORY + -u: USER_ID + -d: the date + -i: the name of the instructor + -n: the name of the participant + +The script then creates $(OUTPUT_DIRECTORY)/$(BADGE_TYPE)/$(USER_ID).pdf as +output. + +This script will also take a CSV file as input. The file must contain rows of: + + name[,badge][,instructor][,user_id][,date] + +such as: + badge,instructor,user_id,name,date + swc-instructor,Grace Hopper,turing_alan,Alan Turing,2016-01-27 + +Any missing columns are filled in from the command line optiomns. The order of +the columns does not matter. +''' + +import sys +import pandas +import tempfile +import string +import argparse +from pathlib import Path +from datetime import date +import cairosvg +from jinja2 import Environment, PackageLoader, select_autoescape + + +DATE_FORMAT = '%B %-d, %Y' + + +def main(): + args = parse_args() + + env = Environment( + loader=PackageLoader("learner_certificates"), + autoescape=select_autoescape()) + + if args.csv_file: + check(args.csv_file.exists(), f"no such file {args.csv_file}") + process_csv(args, env) + else: + process_single(args, env) + + +def construct_user_name(name): + '''construct a username from the name by only allowing + alphanumeric characters and replacing spaces with _''' + valid_characters = string.ascii_letters + string.digits + '_' + user_id = ''.join( + ch for ch in name.replace(' ', '_') if ch in valid_characters) + user_id = user_id.lower() + return user_id + + +def parse_args(): + '''Get command-line arguments.''' + + parser = argparse.ArgumentParser() + parser.add_argument( + '-b', '--badge', help='Type of badge') + parser.add_argument( + '-o', '--output-dir', default=Path.cwd(), type=Path, + help='Output directory (current by default)') + parser.add_argument( + '-d', '--date', default=date.isoformat(date.today()), + help='Date of certificate, defaults to today') + parser.add_argument( + '-i', '--instructor', help='Name of instructor') + grp = parser.add_mutually_exclusive_group(required=True) + grp.add_argument( + '-c', '--csv', type=Path, dest='csv_file', help='CSV file') + grp.add_argument( + '-n', '--name', help='Name of the participant') + parser.add_argument( + '-u', '--userid', dest='user_id', + help='User ID, default construct from name') + args = parser.parse_args() + + return args + + +def process_csv(args, env): + '''Process a CSV file.''' + + data = pandas.read_csv(args.csv_file) + if 'instructor' not in data.columns: + check(args.instructor is not None, "need to specify instructor") + data['instructor'] = args.instructor + if 'date' not in data.columns: + data['date'] = args.date + if 'user_id' not in data.columns: + data['user_id'] = data['name'].apply(construct_user_name) + if 'badge' not in data.columns: + check(args.badge is not None, "need to specify badge type") + data['badge'] = args.badge + for _, row in data.iterrows(): + create_certificate(args.output_dir, env, row) + + +def process_single(args, env): + '''Process a single entry.''' + + check(args.instructor is not None, "need to specify instructor") + check(args.badge is not None, "need to specify badge type") + + if args.user_id is None: + user_id = construct_user_name(args.name) + else: + user_id = args.user_id + + params = {} + for k in ['badge', 'instructor', 'name', 'date']: + params[k] = getattr(args, k) + params['user_id'] = user_id + + create_certificate(args.output_dir, env, params) + + +def create_certificate(output, env, params): + '''Create a single certificate.''' + + params['date'] = date.fromisoformat(params['date']).strftime(DATE_FORMAT) + + template = env.get_template(params['badge'] + ".svg") + badge_path = output / Path(params['badge']) + + if not badge_path.exists(): + badge_path.mkdir() + outputpdf = badge_path / Path(params['user_id']).with_suffix('.pdf') + + tmp = tempfile.NamedTemporaryFile(suffix='.svg', delete=False) + tmp.write(bytes(template.render(**params), 'utf-8')) + cairosvg.svg2pdf(url=tmp.name, write_to=str(outputpdf), dpi=90) + + Path(tmp.name).unlink() + + +def check(condition, message): + '''Fail if condition not met.''' + + if not condition: + print(message, file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/dc-attendance.svg b/learner_certificates/templates/dc-attendance.svg similarity index 100% rename from dc-attendance.svg rename to learner_certificates/templates/dc-attendance.svg diff --git a/lc-attendance.svg b/learner_certificates/templates/lc-attendance.svg similarity index 100% rename from lc-attendance.svg rename to learner_certificates/templates/lc-attendance.svg diff --git a/swc-attendance.svg b/learner_certificates/templates/swc-attendance.svg similarity index 100% rename from swc-attendance.svg rename to learner_certificates/templates/swc-attendance.svg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 3ef89d8..41d1222 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ jinja2 -jinja2-cli -svglib +cairosvg +pandas diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..888ea5c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +[metadata] +name = learner-certificates +version = attr: learner_certificates.__version__ +author = Magnus Hagdorn +author_email = magnus.hagdorn@charite.de +url = https://github.com/ScientificComputingCharite/learner-certificates +description = generating Carpentries certificates +long_description = file: README.md +long_description_content_type = text/markdown + +classifiers = + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Topic :: Education + Intended Audience :: Education + +[options] +packages = + learner_certificates +zip_safe = True +include_package_data = True +install_requires = + pandas + jinja2 + +lint = flake8 >= 3.5.0 + +[options.entry_points] +console_scripts = + generate_carpentries_certificate = learner_certificates.certificates:main + +[options.package_data] +learner_certificates = templates/*.svg diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup()