From 065728f1fc216d213871d205a7d405e34979411d Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 14:18:44 -0400 Subject: [PATCH 1/7] this catches up the openedx cert repo with some recent changes made to the private one --- gen_cert.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/gen_cert.py b/gen_cert.py index 654dd7b2..a2288d88 100644 --- a/gen_cert.py +++ b/gen_cert.py @@ -408,6 +408,9 @@ def _generate_certificate(self, student_name, download_dir, elif self.template_version == 2: return self._generate_v2_certificate(student_name, download_dir, verify_dir, filename) + elif self.template_version == 'MIT_PE': + return self._generate_mit_pe_certificate(student_name, download_dir, verify_dir, filename) + def _generate_v1_certificate(self, student_name, download_dir, verify_dir, filename='Certificate.pdf'): # A4 page size is 297mm x 210mm @@ -1023,6 +1026,143 @@ def _generate_v2_certificate(self, student_name, download_dir, return (download_uuid, verify_uuid, download_url) + def _generate_mit_pe_certificate(self, student_name, download_dir, verify_dir, filename='Certificate.pdf'): + """ + Generate the BigDataX certs + """ + # 8.5x11 page size 279.4mm x 215.9mm + WIDTH = 279 # width in mm (8.5x11) + HEIGHT = 216 # height in mm (8.5x11) + + download_uuid = uuid.uuid4().hex + verify_uuid = uuid.uuid4().hex + download_url = "https://s3.amazonaws.com/{0}/" \ + "{1}/{2}/{3}".format( + BUCKET, S3_CERT_PATH, + download_uuid, filename) + + filename = os.path.join(download_dir, download_uuid, filename) + + # This file is overlaid on the template certificate + overlay_pdf_buffer = StringIO.StringIO() + c = canvas.Canvas(overlay_pdf_buffer) + c.setPageSize((WIDTH * mm, HEIGHT * mm)) + + # register all fonts in the fonts/ dir, + # there are more fonts in here than we need + # but the performance hit seems minimal + + for font_file in glob('{0}/fonts/*.ttf'.format(self.template_dir)): + font_name = os.path.basename(os.path.splitext(font_file)[0]) + pdfmetrics.registerFont(TTFont(font_name, font_file)) + + #### STYLE: grid/layout + LEFT_INDENT = 10 # mm from the left side to write the text + MAX_WIDTH = 260 # maximum width on the content in the cert, used for wrapping + + #### STYLE: template-wide typography settings + style_type_name_size = 36 + style_type_name_leading = 53 + style_type_name_med_size = 22 + style_type_name_med_leading = 27 + style_type_name_small_size = 18 + style_type_name_small_leading = 21 + + #### STYLE: template-wide color settings + style_color_name = colors.Color(0.000000, 0.000000, 0.000000) + + #### STYLE: positioning + pos_name_y = 137 + pos_name_med_y = 142 + pos_name_small_y = 140 + pos_name_no_wrap_offset_y = 2 + + #### HTML Parser #### + # Since the final string is HTML in a PDF we need to un-escape the html + # when calculating the string width. + html = HTMLParser() + + ####### ELEM: Student Name + # default is to use Garamond for the name, + # will fall back to Arial if there are + # unusual characters + y_offset_name = pos_name_y + y_offset_name_med = pos_name_med_y + y_offset_name_small = pos_name_small_y + + styleUnicode = ParagraphStyle(name="arial", leading=10, + fontName='Arial Unicode') + styleGaramondStudentName = ParagraphStyle(name="garamond", fontName='Garamond-Bold') + styleGaramondStudentName.leading = style_type_name_small_size + + style = styleGaramondStudentName + + html_student_name = html.unescape(student_name) + larger_width = stringWidth(html_student_name.decode('utf-8'), + 'Garamond-Bold', style_type_name_size) / mm + smaller_width = stringWidth(html_student_name.decode('utf-8'), + 'Garamond-Bold', style_type_name_small_size) / mm + + paragraph_string = arabic_reshaper.reshape(student_name.decode('utf-8')) + paragraph_string = get_display(paragraph_string) + + # Garamond only supports Latin-1 + # if we can't use it, use Gentium + if self._use_unicode_font(student_name): + style = styleUnicode + larger_width = stringWidth(html_student_name.decode('utf-8'), + 'Arial Unicode', style_type_name_size) / mm + + # if the name is too long, shrink the font size + if larger_width < MAX_WIDTH: + style.fontSize = style_type_name_size + style.leading = style_type_name_leading + y_offset = y_offset_name + elif smaller_width < MAX_WIDTH: + y_offset = y_offset_name_med + pos_name_no_wrap_offset_y + style.fontSize = style_type_name_med_size + style.leading = style_type_name_med_leading + else: + y_offset = y_offset_name_small + style.fontSize = style_type_name_small_size + style.leading = style_type_name_small_leading + style.textColor = style_color_name + style.alignment = TA_CENTER + + paragraph = Paragraph(paragraph_string, style) + paragraph.wrapOn(c, MAX_WIDTH * mm, HEIGHT * mm) + paragraph.drawOn(c, LEFT_INDENT * mm, y_offset * mm) + + ## Generate the final PDF + c.showPage() + c.save() + + # Merge the overlay with the template, then write it to file + output = PdfFileWriter() + overlay = PdfFileReader(overlay_pdf_buffer) + + # We need a page to overlay on. + # So that we don't have to open the template + # several times, we open a blank pdf several times instead + # (much faster) + + blank_pdf = PdfFileReader( + file("{0}/blank-letter.pdf".format(self.template_dir), "rb") + ) + + final_certificate = blank_pdf.getPage(0) + final_certificate.mergePage(self.template_pdf.getPage(0)) + final_certificate.mergePage(overlay.getPage(0)) + + output.addPage(final_certificate) + + self._ensure_dir(filename) + + outputStream = file(filename, "wb") + output.write(outputStream) + outputStream.close() + return (download_uuid, verify_uuid, download_url) + def _generate_verification_page(self, name, filename, From a1fd80de0491e6b0ceea5a21829c4d752eb397e7 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 14:58:04 -0400 Subject: [PATCH 2/7] configuration updates and changes to deprecate the old cert repo --- requirements.txt | 1 + settings.py | 59 +++++++++++++++++++----------------------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 05468dc4..37f86a0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ argparse==1.2.1 +PyYAML==3.11 boto==2.27.0 lockfile==0.9.1 logging-config==1.0.4 diff --git a/settings.py b/settings.py index 2e783f49..9b169172 100644 --- a/settings.py +++ b/settings.py @@ -6,15 +6,26 @@ import json import os +import yaml from logsettings import get_logger_config from path import path ROOT_PATH = path(__file__).dirname() REPO_PATH = ROOT_PATH ENV_ROOT = REPO_PATH.dirname() -# Override TEMPLATE_DATA_DIR if you have have private templates, fonts, etc. -# Needs to be relative to the certificates repo root -TEMPLATE_DATA_DIR = 'template_data' + +# Override CERT_PRIVATE_DIR if you have have private templates, fonts, etc. +CERT_PRIVATE_DIR = REPO_PATH + +# If CERT_PRIVATE_DIR is set in the environment use it + +if 'CERT_PRIVATE_DIR' in os.environ: + CERT_PRIVATE_DIR = path(os.environ['CERT_PRIVATE_DIR']) + +# This directory and file must exist in CERT_PRIVATE_DIR +# if you are using custom templates and custom cert config +TEMPLATE_DATA_SUBDIR = 'template_data' +CERT_DATA_FILE = 'cert-data.yml' # DEFAULTS DEBUG = False @@ -24,37 +35,6 @@ dev_env=True, debug=True) -# Default long names, these can be overridden in -# env.json -# Full list of courses: -# 'BerkeleyX/CS169.1x/2012_Fall', -# 'BerkeleyX/CS169.2x/2012_Fall', -# 'BerkeleyX/CS188.1x/2012_Fall', -# 'BerkeleyX/CS184.1x/2012_Fall', -# 'HarvardX/CS50x/2012', -# 'HarvardX/PH207x/2012_Fall', -# 'MITx/3.091x/2012_Fall', -# 'MITx/6.002x/2012_Fall', -# 'MITx/6.00x/2012_Fall', -# 'BerkeleyX/CS169/fa12', -# 'BerkeleyX/CS188/fa12', -# 'HarvardX/CS50/2012H', -# 'MITx/3.091/MIT_2012_Fall', -# 'MITx/6.00/MIT_2012_Fall', -# 'MITx/6.002x-EE98/2012_Fall_SJSU', -# 'MITx/6.002x-NUM/2012_Fall_NUM'] - -# What we support: - -CERT_DATA = { - "edX/Open_DemoX/edx_demo_course" : { - "LONG_ORG" : "Sample Org", - "LONG_COURSE" : "Sample course", - "ISSUED_DATE" : "Jan. 1st, 1970" - }, -} - - # Default for the gpg dir # Specify the CERT_KEY_ID before running the test suite CERT_GPG_DIR = '{0}/.gnupg'.format(os.environ['HOME']) @@ -106,7 +86,7 @@ local_loglevel=local_loglevel, debug=False, service_variant=os.environ.get('SERVICE_VARIANT', None)) - TEMPLATE_DATA_DIR = ENV_TOKENS.get('TEMPLATE_DATA_DIR', TEMPLATE_DATA_DIR) + CERT_PRIVATE_DIR = ENV_TOKENS.get('CERT_PRIVATE_DIR', CERT_PRIVATE_DIR) if os.path.isfile(ENV_ROOT / "auth.json"): with open(ENV_ROOT / "auth.json") as env_file: @@ -118,4 +98,11 @@ CERT_AWS_KEY = ENV_TOKENS.get('CERT_AWS_KEY', CERT_AWS_KEY) CERT_AWS_ID = ENV_TOKENS.get('CERT_AWS_ID', CERT_AWS_ID) -TEMPLATE_DIR = os.path.join(REPO_PATH, TEMPLATE_DATA_DIR) + +# Use the custom CERT_PRIVATE_DIR for paths to the +# template sub directory and the cert data config + +TEMPLATE_DIR = CERT_PRIVATE_DIR / TEMPLATE_DATA_SUBDIR + +with open(CERT_PRIVATE_DIR / CERT_DATA_FILE) as f: + CERT_DATA = yaml.load(f.read()) From da6e9dee7d5681c4d5e95f3ee49d2f61ba21459c Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 14:58:33 -0400 Subject: [PATCH 3/7] adding cert-data.yml to replace the config in settings --- cert-data.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 cert-data.yml diff --git a/cert-data.yml b/cert-data.yml new file mode 100644 index 00000000..6f52ba33 --- /dev/null +++ b/cert-data.yml @@ -0,0 +1,5 @@ +--- +edX/Open_DemoX/edx_demo_course: + ISSUED_DATE: Jan. 1st, 1970 + LONG_COURSE: Sample course + LONG_ORG: Sample Org From fa54f00970d30025fd51172fe6bb38d03534d8db Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 15:23:27 -0400 Subject: [PATCH 4/7] adding utf-8 encoding for unicode in the yaml file --- gen_cert.py | 6 +++--- settings.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gen_cert.py b/gen_cert.py index a2288d88..e9404600 100644 --- a/gen_cert.py +++ b/gen_cert.py @@ -100,15 +100,15 @@ def __init__(self, course_id, template_pdf=None, aws_id=None, aws_key=None, # lookup long names from the course_id try: if long_org is None: - self.long_org = settings.CERT_DATA[course_id]['LONG_ORG'] + self.long_org = settings.CERT_DATA[course_id]['LONG_ORG'].encode('utf-8') else: self.long_org = long_org if long_course is None: - self.long_course = settings.CERT_DATA[course_id]['LONG_COURSE'] + self.long_course = settings.CERT_DATA[course_id]['LONG_COURSE'].encode('utf-8') else: self.long_course = long_course if issued_date is None: - self.issued_date = settings.CERT_DATA[course_id]['ISSUED_DATE'] + self.issued_date = settings.CERT_DATA[course_id]['ISSUED_DATE'].encode('utf-8') else: self.issued_date = issued_date diff --git a/settings.py b/settings.py index 9b169172..00ffa294 100644 --- a/settings.py +++ b/settings.py @@ -7,9 +7,11 @@ import json import os import yaml + from logsettings import get_logger_config from path import path + ROOT_PATH = path(__file__).dirname() REPO_PATH = ROOT_PATH ENV_ROOT = REPO_PATH.dirname() From 7662e4af8991041c1b3d02790542e4459902e8ac Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 15:25:35 -0400 Subject: [PATCH 5/7] reducing loglevel so boto is less chatty --- settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 00ffa294..f776b849 100644 --- a/settings.py +++ b/settings.py @@ -33,9 +33,9 @@ DEBUG = False LOGGING = get_logger_config(ENV_ROOT, logging_env="dev", - local_loglevel="DEBUG", + local_loglevel="INFO", dev_env=True, - debug=True) + debug=False) # Default for the gpg dir # Specify the CERT_KEY_ID before running the test suite From ad2e6195798560e8b2a95c8b0276de990ef0e4b3 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 15:32:34 -0400 Subject: [PATCH 6/7] updating path for example-key-ownertrust --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1e09c660..35fa5e32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ install: script: - | curl https://raw.githubusercontent.com/edx/configuration/master/playbooks/roles/certs/files/example-private-key.txt -o /var/tmp/key.txt - curl https://raw.githubusercontent.com/edx/configuration/rc/injera/playbooks/roles/certs/files/example-key-ownertrust.txt -o /var/tmp/trust.txt + curl https://raw.githubusercontent.com/edx/configuration/master/playbooks/roles/certs/files/example-key-ownertrust.txt -o /var/tmp/trust.txt /usr/bin/gpg --import /var/tmp/key.txt /usr/bin/gpg --import-ownertrust /var/tmp/trust.txt nosetests tests.gen_cert_test:test_cert_gen From 6f1aba521d1347fcec3ce953d523b29853b61497 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Wed, 30 Apr 2014 15:46:02 -0400 Subject: [PATCH 7/7] updating download_url formatting --- gen_cert.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gen_cert.py b/gen_cert.py index e9404600..9467236a 100644 --- a/gen_cert.py +++ b/gen_cert.py @@ -1036,10 +1036,10 @@ def _generate_mit_pe_certificate(self, student_name, download_dir, verify_dir, f download_uuid = uuid.uuid4().hex verify_uuid = uuid.uuid4().hex - download_url = "https://s3.amazonaws.com/{0}/" \ - "{1}/{2}/{3}".format( - BUCKET, S3_CERT_PATH, - download_uuid, filename) + download_url = "{base_url}/{cert}/{uuid}/{file}".format( + base_url=settings.CERT_DOWNLOAD_URL, + cert=S3_CERT_PATH, uuid=download_uuid, file=filename + ) filename = os.path.join(download_dir, download_uuid, filename)