diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..1e09c660 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +# Travis CI configuration file for running tests +language: python +python: + - "2.7" +install: + - "pip install --allow-all-external -r requirements.txt" +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 + /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 diff --git a/README.md b/README.md index 582b1a13..f5af6579 100644 --- a/README.md +++ b/README.md @@ -136,12 +136,20 @@ To run the test suite: CERT_AWS_ID = # Amazon Web Services ID CERT_AWS_KEY = # Amazon Web Services Key CERT_BUCKET = # Amazon Web Services S3 bucket name - -2. From the `certificates` directory, run: + It is also acceptable to leave the AWS KEY and ID values as none and instead + use .boto file or run this code from a server that has an + IAM role that gives it write access to the bucket in the configuration. + +2. To run all of the tests from the `certificates` directory, run: nosetests + Note that this will run tests that will fail unless AWS credentials are setup. To run just + the tests for local on-disk publishing run: + + nosetests tests.gen_cert_test:test_cert_gen + **Troubleshooting**: If tests fail with errors, try running: diff --git a/create_pdfs.py b/create_pdfs.py index 9f246289..b3b8f97e 100644 --- a/create_pdfs.py +++ b/create_pdfs.py @@ -87,7 +87,7 @@ def main(): ) (download_uuid, verify_uuid, download_url) = cert.create_and_upload( - name, upload=True, cleanup=False) + name, upload=True, copy_to_webroot=False, cleanup=False) certificate_data.append([name, download_url]) gen_dir = os.path.join( cert.dir_prefix, S3_CERT_PATH, download_uuid) diff --git a/gen_cert.py b/gen_cert.py index d0a10415..654dd7b2 100644 --- a/gen_cert.py +++ b/gen_cert.py @@ -24,20 +24,20 @@ import logging.config import reportlab.rl_config import tempfile -from simples3 import S3Bucket +import boto.s3 +from boto.s3.key import Key from bidi.algorithm import get_display import arabic_reshaper reportlab.rl_config.warnOnMissingFontGlyphs = 0 -BUCKET = settings.CERT_BUCKET logging.config.dictConfig(settings.LOGGING) log = logging.getLogger('certificates.' + __name__) # name of the S3 bucket # paths to the S3 are for downloading and verifying certs S3_CERT_PATH = 'downloads' S3_VERIFY_PATH = 'cert' - +BUCKET = settings.CERT_BUCKET # reduce logging level for gnupg l = logging.getLogger('gnupg') l.setLevel('WARNING') @@ -158,7 +158,7 @@ def __init__(self, course_id, template_pdf=None, aws_id=None, aws_key=None, if template_pdf: # Open and load the template pdf for the org self.template_pdf = PdfFileReader( - file(os.path.join(template_path, template_pdf), 'rb')) + open(os.path.join(template_path, template_pdf), 'rb')) else: # For backwards compatibility and standalone testing # when the template file is not available use the @@ -170,9 +170,13 @@ def __init__(self, course_id, template_pdf=None, aws_id=None, aws_key=None, course_id.split('/')[0], course_id.split('/')[1])), "rb")) # Open the 188 letterhead pdf - self.letterhead_pdf = PdfFileReader( - file("{0}/letterhead-template-BerkeleyX-CS188.1x.pdf".format( - self.template_dir), "rb")) + # if it exists + letterhead_path = "{0}/letterhead-template-BerkeleyX-CS188.1x.pdf".format(self.template_dir) + + if os.path.exists(letterhead_path): + self.letterhead_pdf = PdfFileReader(file(letterhead_path, "rb")) + else: + self.letterhead_pdf = None self.aws_id = aws_id self.aws_key = aws_key @@ -181,8 +185,9 @@ def delete_certificate(self, delete_download_uuid, delete_verify_uuid): # TODO remove/archive an existing certificate raise NotImplementedError - def create_and_upload(self, name, upload=True, cleanup=True, - letterhead=False): + def create_and_upload(self, name, upload=settings.S3_UPLOAD, cleanup=True, + copy_to_webroot=settings.COPY_TO_WEB_ROOT, + cert_web_root=settings.CERT_WEB_ROOT, letterhead=False): """ name - Full name that will be on the certificate upload - Upload to S3 (defaults to True) @@ -222,29 +227,35 @@ def create_and_upload(self, name, upload=True, cleanup=True, verify_dir=verify_path) # upload generated certificate and verification files to S3 - if upload: - base_url = 'http://{0}.s3.amazonaws.com'.format(BUCKET) - s3 = S3Bucket(BUCKET, - access_key=str(self.aws_id), - secret_key=str(self.aws_key), - base_url=base_url) - - for dirpath, dirnames, filenames in os.walk(self.dir_prefix): - for filename in filenames: - aws_path = os.path.relpath(os.path.join(dirpath, filename), - start=self.dir_prefix) - local_path = os.path.join(dirpath, filename) + for dirpath, dirnames, filenames in os.walk(self.dir_prefix): + for filename in filenames: + local_path = os.path.join(dirpath, filename) + dest_path = os.path.relpath( + os.path.join(dirpath, filename), + start=self.dir_prefix + ) + if upload: + s3_conn = boto.connect_s3() + bucket = s3_conn.get_bucket(BUCKET) + key = Key(bucket, name=dest_path) log.info('uploading to {0} from {1} to {2}'.format( - base_url, local_path, aws_path)) - with open(local_path) as f: - s3.put(aws_path, f.read(), acl="public-read") + settings.CERT_URL, local_path, dest_path)) + key.set_contents_from_filename(local_path, policy='public-read') + + if copy_to_webroot: + publish_dest = os.path.join(cert_web_root, dest_path) + log.info('publishing to {0} from {1} to {2}'.format( + settings.CERT_URL, local_path, publish_dest)) + if not os.path.exists(os.path.dirname(publish_dest)): + os.makedirs(os.path.dirname(publish_dest)) + shutil.copy(local_path, publish_dest) + if cleanup: if os.path.exists(self.dir_prefix): shutil.rmtree(self.dir_prefix) return (download_uuid, verify_uuid, download_url) - def _generate_letterhead(self, student_name, download_dir, filename='distinction-letter.pdf'): @@ -259,10 +270,10 @@ def _generate_letterhead(self, student_name, download_dir, # A4 page size is 210mm x 297mm download_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) @@ -271,13 +282,12 @@ def _generate_letterhead(self, student_name, download_dir, c = canvas.Canvas(overlay_pdf_buffer) c.setPageSize((297 * mm, 210 * 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)): + # the open-source repo does not include + # a font that has full unicode support. + 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)) @@ -295,17 +305,18 @@ def _generate_letterhead(self, student_name, download_dir, addMapping('OpenSans-Regular', 1, 0, 'OpenSans-Bold') addMapping('OpenSans-Regular', 1, 1, 'OpenSans-BoldItalic') - - styleArial = ParagraphStyle(name="arial", leading=10, - fontName='Arial Unicode') - styleOpenSans = ParagraphStyle(name="opensans-regular", leading=10, - fontName='OpenSans-Regular') - styleOpenSansLight = ParagraphStyle(name="opensans-light", leading=10, - fontName='OpenSans-Light') - styleUCBerkeley = ParagraphStyle(name="ucberkeley", leading=10, - fontName='UCBerkeleyOS') - - + styleArial = ParagraphStyle( + name="arial", leading=10, + fontName='Arial Unicode' + ) + styleOpenSans = ParagraphStyle( + name="opensans-regular", + leading=10, fontName='OpenSans-Regular' + ) + styleOpenSansLight = ParagraphStyle( + name="opensans-light", + leading=10, fontName='OpenSans-Light' + ) # Text is overlayed top to bottom # * Student's name @@ -322,8 +333,9 @@ def _generate_letterhead(self, student_name, download_dir, # will fall back to Arial if there are # unusual characters style = styleOpenSans - width = stringWidth(student_name.decode('utf-8'), - 'OpenSans-Bold', 16) / mm + width = stringWidth( + student_name.decode('utf-8'), + 'OpenSans-Bold', 16) / mm paragraph_string = "{0}".format(student_name) if self._use_unicode_font(student_name): @@ -335,7 +347,7 @@ def _generate_letterhead(self, student_name, download_dir, style.fontSize = 16 style.textColor = colors.Color( - 0, 0.624, 0.886) + 0, 0.624, 0.886) style.alignment = TA_LEFT paragraph = Paragraph(paragraph_string, style) @@ -346,14 +358,12 @@ def _generate_letterhead(self, student_name, download_dir, style = styleOpenSansLight style.fontSize = 14 style.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) # Place the comma after the student's name paragraph = Paragraph(",", style) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) paragraph.drawOn(c, (LEFT_INDENT + width) * mm, 216.8 * mm) - - c.showPage() c.save() @@ -367,7 +377,7 @@ def _generate_letterhead(self, student_name, download_dir, # (much faster) blank_pdf = PdfFileReader( - file("{0}/blank-portrait.pdf".format(self.template_dir), "rb")) + file("{0}/blank-portrait.pdf".format(self.template_dir), "rb")) final_certificate = blank_pdf.getPage(0) final_certificate.mergePage(self.letterhead_pdf.getPage(0)) @@ -383,9 +393,8 @@ def _generate_letterhead(self, student_name, download_dir, return (download_uuid, download_url) - def _generate_certificate(self, student_name, download_dir, - verify_dir, filename='Certificate.pdf'): + verify_dir, filename='Certificate.pdf'): """ Generate a PDF certificate, signature and static html files used for validation. @@ -393,19 +402,21 @@ def _generate_certificate(self, student_name, download_dir, return (download_uuid, verify_uuid, download_url) """ + if self.template_version == 1: + return self._generate_v1_certificate(student_name, download_dir, verify_dir, filename) - if self.template_version == 2: + elif self.template_version == 2: return self._generate_v2_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 verify_uuid = uuid.uuid4().hex download_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) # This file is overlaid on the template certificate @@ -413,13 +424,11 @@ def _generate_certificate(self, student_name, download_dir, c = canvas.Canvas(overlay_pdf_buffer) c.setPageSize((297 * mm, 210 * 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)): + 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)) @@ -437,17 +446,18 @@ def _generate_certificate(self, student_name, download_dir, addMapping('OpenSans-Regular', 1, 0, 'OpenSans-Bold') addMapping('OpenSans-Regular', 1, 1, 'OpenSans-BoldItalic') - - styleArial = ParagraphStyle(name="arial", leading=10, - fontName='Arial Unicode') - styleOpenSans = ParagraphStyle(name="opensans-regular", leading=10, - fontName='OpenSans-Regular') - styleOpenSansLight = ParagraphStyle(name="opensans-light", leading=10, - fontName='OpenSans-Light') - styleUCBerkeley = ParagraphStyle(name="ucberkeley", leading=10, - fontName='UCBerkeleyOS') - - + styleArial = ParagraphStyle( + name="arial", leading=10, + fontName='Arial Unicode' + ) + styleOpenSans = ParagraphStyle( + name="opensans-regular", leading=10, + fontName='OpenSans-Regular' + ) + styleOpenSansLight = ParagraphStyle( + name="opensans-light", leading=10, + fontName='OpenSans-Light' + ) # Text is overlayed top to bottom # * Issued date (top right corner) @@ -461,51 +471,52 @@ def _generate_certificate(self, student_name, download_dir, HEIGHT = 210 # hight in mm (A4) LEFT_INDENT = 49 # mm from the left side to write the text - RIGHT_INDENT = 49 # mm from the right side for the CERTIFICATE + RIGHT_INDENT = 49 # mm from the right side for the CERTIFICATE ####### CERTIFICATE styleOpenSansLight.fontSize = 19 styleOpenSansLight.leading = 10 styleOpenSansLight.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) styleOpenSansLight.alignment = TA_LEFT paragraph_string = "CERTIFICATE" # Right justified so we compute the width - width = stringWidth(paragraph_string, - 'OpenSans-Light', 19) / mm + width = stringWidth( + paragraph_string, + 'OpenSans-Light', 19) / mm paragraph = Paragraph("{0}".format( paragraph_string), styleOpenSansLight) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) - paragraph.drawOn(c, (WIDTH-RIGHT_INDENT-width) * mm, 163 * mm) - + paragraph.drawOn(c, (WIDTH - RIGHT_INDENT - width) * mm, 163 * mm) ####### Issued .. styleOpenSansLight.fontSize = 12 styleOpenSansLight.leading = 10 styleOpenSansLight.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) styleOpenSansLight.alignment = TA_LEFT paragraph_string = "Issued {0}".format(self.issued_date) # Right justified so we compute the width - width = stringWidth(paragraph_string, - 'OpenSans-LightItalic', 12) / mm + width = stringWidth( + paragraph_string, + 'OpenSans-LightItalic', 12) / mm paragraph = Paragraph("{0}".format( paragraph_string), styleOpenSansLight) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) - paragraph.drawOn(c, (WIDTH-RIGHT_INDENT-width) * mm, 155 * mm) + paragraph.drawOn(c, (WIDTH - RIGHT_INDENT - width) * mm, 155 * mm) ####### This is to certify.. styleOpenSansLight.fontSize = 12 styleOpenSansLight.leading = 10 styleOpenSansLight.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) styleOpenSansLight.alignment = TA_LEFT paragraph_string = "This is to certify that" @@ -520,8 +531,9 @@ def _generate_certificate(self, student_name, download_dir, # unusual characters style = styleOpenSans style.leading = 10 - width = stringWidth(student_name.decode('utf-8'), - 'OpenSans-Bold', 34) / mm + width = stringWidth( + student_name.decode('utf-8'), + 'OpenSans-Bold', 34) / mm paragraph_string = "{0}".format(student_name) if self._use_unicode_font(student_name): @@ -541,7 +553,7 @@ def _generate_certificate(self, student_name, download_dir, nameYOffset = 124.5 style.textColor = colors.Color( - 0, 0.624, 0.886) + 0, 0.624, 0.886) style.alignment = TA_LEFT paragraph = Paragraph(paragraph_string, style) @@ -553,7 +565,7 @@ def _generate_certificate(self, student_name, download_dir, styleOpenSansLight.fontSize = 12 styleOpenSansLight.leading = 10 styleOpenSansLight.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) styleOpenSansLight.alignment = TA_LEFT paragraph_string = "successfully completed" @@ -592,11 +604,11 @@ def _generate_certificate(self, student_name, download_dir, styleOpenSans.fontSize = 24 styleOpenSans.leading = 10 styleOpenSans.textColor = colors.Color( - 0, 0.624, 0.886) + 0, 0.624, 0.886) styleOpenSans.alignment = TA_LEFT paragraph_string = "{0}: {1}".format( - self.course, self.long_course) + self.course, self.long_course) paragraph = Paragraph(paragraph_string, styleOpenSans) # paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) if 'PH207x' in self.course: @@ -609,12 +621,11 @@ def _generate_certificate(self, student_name, download_dir, paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) paragraph.drawOn(c, LEFT_INDENT * mm, 99 * mm) - ###### A course of study.. styleOpenSansLight.fontSize = 12 styleOpenSansLight.textColor = colors.Color( - 0.302, 0.306, 0.318) + 0.302, 0.306, 0.318) styleOpenSansLight.alignment = TA_LEFT paragraph_string = "a course of study offered by {0}" \ @@ -636,13 +647,14 @@ def _generate_certificate(self, student_name, download_dir, paragraph_string = "HONOR CODE CERTIFICATE
" \ "*Authenticity of this certificate can be verified at " \ - "" \ - "https://{bucket}/{verify_path}/{verify_uuid}" - - paragraph_string = paragraph_string.format(bucket=BUCKET, - verify_path=S3_VERIFY_PATH, - verify_uuid=verify_uuid) + "" \ + "{verify_url}/{verify_path}/{verify_uuid}" + paragraph_string = paragraph_string.format( + verify_url=settings.CERT_VERIFY_URL, + verify_path=S3_VERIFY_PATH, + verify_uuid=verify_uuid + ) paragraph = Paragraph(paragraph_string, styleOpenSansLight) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) @@ -695,16 +707,15 @@ def _generate_v2_certificate(self, student_name, download_dir, """ # 8.5x11 page size 279.4mm x 215.9mm - WIDTH = 279 # width in mm (8.5x11) + WIDTH = 279 # width in mm (8.5x11) HEIGHT = 216 # height in mm (8.5x11) verify_uuid = uuid.uuid4().hex download_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) # This file is overlaid on the template certificate @@ -746,15 +757,13 @@ def _generate_v2_certificate(self, student_name, download_dir, #### STYLE: grid/layout LEFT_INDENT = 23 # mm from the left side to write the text - RIGHT_INDENT = 23 # mm from the right side for the CERTIFICATE - MAX_WIDTH = 150 # maximum width on the content in the cert, used for wrapping + MAX_WIDTH = 150 # maximum width on the content in the cert, used for wrapping #### STYLE: template-wide typography settings style_type_metacopy_size = 13 style_type_metacopy_leading = 10 style_type_footer_size = 8 - style_type_footer_leading = 10 style_type_name_size = 36 style_type_name_leading = 53 @@ -765,16 +774,12 @@ def _generate_v2_certificate(self, student_name, download_dir, style_type_course_size = 24 style_type_course_leading = 28 - style_type_course_med_size = 22 - style_type_course_med_leading = 20 style_type_course_small_size = 16 style_type_course_small_leading = 20 #### STYLE: template-wide color settings style_color_metadata = colors.Color(0.541176, 0.509804, 0.560784) style_color_name = colors.Color(0.000000, 0.000000, 0.000000) - style_color_course = colors.Color(0.000000, 0.000000, 0.000000) - style_color_footer = colors.Color(0.000000, 0.000000, 0.000000) #### STYLE: positioning pos_metacopy_title_y = 120 @@ -819,7 +824,6 @@ def _generate_v2_certificate(self, student_name, download_dir, paragraph_string = 'This is to certify that' - width = stringWidth(paragraph_string, 'AvenirNext-Regular', style_type_metacopy_size) / mm paragraph = Paragraph(paragraph_string, styleAvenirNext) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) paragraph.drawOn(c, LEFT_INDENT * mm, y_offset * mm) @@ -842,8 +846,9 @@ def _generate_v2_certificate(self, student_name, download_dir, html_student_name = html.unescape(student_name) larger_width = stringWidth(html_student_name.decode('utf-8'), 'AvenirNext-DemiBold', style_type_name_size) / mm - smaller_width = stringWidth(html_student_name.decode('utf-8'), - 'AvenirNext-DemiBold', style_type_name_small_size) / mm + smaller_width = stringWidth( + html_student_name.decode('utf-8'), + 'AvenirNext-DemiBold', style_type_name_small_size) / mm #TODO: get all strings working reshaped and handling bi-directional strings paragraph_string = arabic_reshaper.reshape(student_name.decode('utf-8')) @@ -899,7 +904,7 @@ def _generate_v2_certificate(self, student_name, download_dir, y_offset_larger = pos_course_y y_offset_smaller = pos_course_small_y - styleAvenirCourseName = ParagraphStyle(name="avenirnext-demi",fontName='AvenirNext-DemiBold') + styleAvenirCourseName = ParagraphStyle(name="avenirnext-demi", fontName='AvenirNext-DemiBold') styleAvenirCourseName.textColor = style_color_name if self.template_type == 'verified': styleAvenirCourseName.textColor = v_style_color_course @@ -957,8 +962,6 @@ def _generate_v2_certificate(self, student_name, download_dir, y_offset = pos_footer_date_y paragraph_string = "Issued {0}".format(self.issued_date) # Right justified so we compute the width - width = stringWidth(paragraph_string, - 'AvenirNext-DemiBold', styleAvenirFooter.fontSize) / mm paragraph = Paragraph("{0}".format( paragraph_string), styleAvenirFooter) paragraph.wrapOn(c, WIDTH * mm, HEIGHT * mm) @@ -1053,8 +1056,9 @@ def _generate_verification_page(self, gpg.encoding = 'utf-8' with open(filename) as f: if settings.CERT_KEY_ID: - signed_data = gpg.sign_file(f, detach=True, - keyid=settings.CERT_KEY_ID).data + signed_data = gpg.sign_file( + f, detach=True, + keyid=settings.CERT_KEY_ID).data else: signed_data = gpg.sign_file(f, detach=True).data @@ -1069,16 +1073,17 @@ def _generate_verification_page(self, valid_template = 'v2/valid.html' verify_template = 'v2/verify.html' - # create the validation page - signature_download_url = "https://{0}/{1}/" \ - "{2}/{3}".format( - BUCKET, S3_VERIFY_PATH, - verify_uuid, - os.path.basename(signature_filename)) - verify_page_url = "https://{0}/{1}/" \ - "{2}/verify.html".format(BUCKET, S3_VERIFY_PATH, - verify_uuid) + signature_download_url = "{verify_url}/{verify_path}/{verify_uuid}/{verify_filename}".format( + verify_url=settings.CERT_VERIFY_URL, + verify_path=S3_VERIFY_PATH, + verify_uuid=verify_uuid, + verify_filename=os.path.basename(signature_filename)) + + verify_page_url = "{verify_url}/{verify_path}/{verify_uuid}/verify.html".format( + verify_url=settings.CERT_VERIFY_URL, + verify_path=S3_VERIFY_PATH, + verify_uuid=verify_uuid) type_map = { 'verified': {'type': 'idverified', 'type_name': 'Verified'}, @@ -1121,16 +1126,14 @@ def _generate_verification_page(self, verify_page = f.read().decode('utf-8').format( NAME=name.decode('utf-8'), SIG_URL=signature_download_url, - SIG_FILE=os.path.basename( - signature_download_url), - PDF_FILE=os.path.basename( - download_url)) + SIG_FILE=os.path.basename(signature_download_url), + PDF_FILE=os.path.basename(download_url) + ) with open(os.path.join( output_dir, verify_uuid, "verify.html"), 'w') as f: f.write(verify_page.encode('utf-8')) - def _ensure_dir(self, f): d = os.path.dirname(f) if not os.path.exists(d): @@ -1155,7 +1158,6 @@ def _use_non_latin(self, string): """ return self._contains_characters_above(string, 0x0100) - def _use_unicode_font(self, string): # This function should return true for any # string that that opensans/baskerville can't render. diff --git a/requirements.txt b/requirements.txt index d5517eb5..05468dc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ argparse==1.2.1 -boto==2.6.0 +boto==2.27.0 lockfile==0.9.1 logging-config==1.0.4 nose==1.2.1 diff --git a/settings.py b/settings.py index 18a44290..2e783f49 100644 --- a/settings.py +++ b/settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Setings file for the certificate agent +Settings file for the certificate agent """ import json @@ -12,11 +12,13 @@ ROOT_PATH = path(__file__).dirname() REPO_PATH = ROOT_PATH ENV_ROOT = REPO_PATH.dirname() -TEMPLATE_DIR = '{0}/template_data'.format(REPO_PATH) +# 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' # DEFAULTS DEBUG = False -LOGGING = get_logger_config(ENV_ROOT / "log", +LOGGING = get_logger_config(ENV_ROOT, logging_env="dev", local_loglevel="DEBUG", dev_env=True, @@ -50,34 +52,37 @@ "LONG_COURSE" : "Sample course", "ISSUED_DATE" : "Jan. 1st, 1970" }, - "CaltechX/CS1156x/Fall2013" : { - "LONG_ORG" : "California Institute of Technology", - "LONG_COURSE" : "Learning From Data", - "ISSUED_DATE" : "December 9th, 2013", - "COURSE": "CS1156x", - "COURSE_ASSOCIATION_TEXT" : "a non-credit course", - "VERSION": 2 - }, - "University_of_TorontoX/OEE101x/3T2013" : { - "COURSE" : "OEE101x", - "ORG" : "University of TorontoX", - "LONG_ORG" : "University of Toronto", - "LONG_COURSE" : "Our Energetic Earth", - "ISSUED_DATE" : "December 16th, 2013", - "VERSION": 2 - }, } # Default for the gpg dir # Specify the CERT_KEY_ID before running the test suite CERT_GPG_DIR = '{0}/.gnupg'.format(os.environ['HOME']) -CERT_KEY_ID = 'info@edx.org' +# dummy key - https://raw.githubusercontent.com/edx/configuration/master/playbooks/roles/certs/files/example-private-key.txt +CERT_KEY_ID = 'FEF8D954' # Specify these credentials before running the test suite -CERT_AWS_ID = 'PLEASE_PROVIDE_AN_ID' -CERT_AWS_KEY = 'PLEASE_PROVIDE_AN_AWS_BUCKET_KEY' -CERT_BUCKET = 'provide_a_bucket_name' +# or ensure that your .boto file has write permission +# to the bucket. +CERT_AWS_ID = None +CERT_AWS_KEY = None +# Update this with your bucket name +CERT_BUCKET = 'verify-test.edx.org' +CERT_WEB_ROOT = '/var/tmp' +# when set to true this will copy the generated certificate +# to the CERT_WEB_ROOT. This is not something you want to do +# unless you are running your certificate service on a single +# server +COPY_TO_WEB_ROOT = False +S3_UPLOAD = True +# This is the base URL used for CERT uploads to s3 +CERT_URL = 'http://{}.s3.amazonaws.com'.format(CERT_BUCKET) +# This is the base URL that will be displayed to the user in the dashboard +# It's different than CERT_URL because because CERT_URL will not have a valid +# SSL certificate. +CERT_DOWNLOAD_URL = 'https://s3.amazonaws.com/{}'.format(CERT_BUCKET) +CERT_VERIFY_URL = 'http://s3.amazonaws.com/{}'.format(CERT_BUCKET) + # load settings from env.json and auth.json if os.path.isfile(ENV_ROOT / "env.json"): @@ -90,11 +95,18 @@ CERT_GPG_DIR = ENV_TOKENS.get('CERT_GPG_DIR', CERT_GPG_DIR) CERT_KEY_ID = ENV_TOKENS.get('CERT_KEY_ID', CERT_KEY_ID) CERT_BUCKET = ENV_TOKENS.get('CERT_BUCKET', CERT_BUCKET) + CERT_URL = ENV_TOKENS.get('CERT_URL', CERT_URL) + CERT_VERIFY_URL = ENV_TOKENS.get('CERT_VERIFY_URL', CERT_VERIFY_URL) + CERT_DOWNLOAD_URL = ENV_TOKENS.get('CERT_DOWNLOAD_URL', CERT_DOWNLOAD_URL) + CERT_WEB_ROOT = ENV_TOKENS.get('CERT_WEB_ROOT', CERT_WEB_ROOT) + COPY_TO_WEB_ROOT = ENV_TOKENS.get('COPY_TO_WEB_ROOT', COPY_TO_WEB_ROOT) + S3_UPLOAD = ENV_TOKENS.get('S3_UPLOAD', S3_UPLOAD) LOGGING = get_logger_config(LOG_DIR, logging_env=ENV_TOKENS['LOGGING_ENV'], 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) if os.path.isfile(ENV_ROOT / "auth.json"): with open(ENV_ROOT / "auth.json") as env_file: @@ -105,3 +117,5 @@ QUEUE_AUTH_PASS = ENV_TOKENS.get('QUEUE_AUTH_PASS', '') 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) diff --git a/template_data/fonts/OpenSans-Bold.ttf b/template_data/fonts/OpenSans-Bold.ttf new file mode 100644 index 00000000..fd79d43b Binary files /dev/null and b/template_data/fonts/OpenSans-Bold.ttf differ diff --git a/template_data/fonts/OpenSans-BoldItalic.ttf b/template_data/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 00000000..9bc80095 Binary files /dev/null and b/template_data/fonts/OpenSans-BoldItalic.ttf differ diff --git a/template_data/fonts/OpenSans-ExtraBold.ttf b/template_data/fonts/OpenSans-ExtraBold.ttf new file mode 100644 index 00000000..21f6f84a Binary files /dev/null and b/template_data/fonts/OpenSans-ExtraBold.ttf differ diff --git a/template_data/fonts/OpenSans-ExtraBoldItalic.ttf b/template_data/fonts/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 00000000..31cb6883 Binary files /dev/null and b/template_data/fonts/OpenSans-ExtraBoldItalic.ttf differ diff --git a/template_data/fonts/OpenSans-Italic.ttf b/template_data/fonts/OpenSans-Italic.ttf new file mode 100644 index 00000000..c90da48f Binary files /dev/null and b/template_data/fonts/OpenSans-Italic.ttf differ diff --git a/template_data/fonts/OpenSans-Light.ttf b/template_data/fonts/OpenSans-Light.ttf new file mode 100644 index 00000000..0d381897 Binary files /dev/null and b/template_data/fonts/OpenSans-Light.ttf differ diff --git a/template_data/fonts/OpenSans-LightItalic.ttf b/template_data/fonts/OpenSans-LightItalic.ttf new file mode 100644 index 00000000..68299c4b Binary files /dev/null and b/template_data/fonts/OpenSans-LightItalic.ttf differ diff --git a/template_data/fonts/OpenSans-Regular.ttf b/template_data/fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..db433349 Binary files /dev/null and b/template_data/fonts/OpenSans-Regular.ttf differ diff --git a/template_data/fonts/OpenSans-Semibold.ttf b/template_data/fonts/OpenSans-Semibold.ttf new file mode 100644 index 00000000..1a7679e3 Binary files /dev/null and b/template_data/fonts/OpenSans-Semibold.ttf differ diff --git a/template_data/fonts/OpenSans-SemiboldItalic.ttf b/template_data/fonts/OpenSans-SemiboldItalic.ttf new file mode 100644 index 00000000..59b6d16b Binary files /dev/null and b/template_data/fonts/OpenSans-SemiboldItalic.ttf differ diff --git a/tests/gen_cert_test.py b/tests/gen_cert_test.py index 1e281f16..52d36516 100644 --- a/tests/gen_cert_test.py +++ b/tests/gen_cert_test.py @@ -2,7 +2,6 @@ from gen_cert import CertificateGen from gen_cert import S3_CERT_PATH, S3_VERIFY_PATH from nose.tools import assert_true -from nose.plugins.skip import SkipTest import settings import os import gnupg @@ -14,19 +13,6 @@ VERIFY_FILES = ['valid.html', 'Certificate.pdf.sig', 'verify.html'] DOWNLOAD_FILES = ['Certificate.pdf'] -REQUIRED_SETTINGS = ["CERT_AWS_ID", "CERT_AWS_KEY", "CERT_BUCKET", "CERT_KEY_ID"] - -def skip_if_not_configured(): - """Tests are skipped unless settings.py has been configured - with valid credentials""" - for required in REQUIRED_SETTINGS: - if not hasattr(settings, required): - raise SkipTest - elif getattr(settings, required) is None: - raise SkipTest - else: - pass - def test_cert_gen(): """ @@ -34,29 +20,34 @@ def test_cert_gen(): * Generates a single dummy certificate * Verifies all file artificats are created * Verifies the pdf signature against the detached signature + * Publishes the certificate to a temporary directory """ - skip_if_not_configured() for course_id in settings.CERT_DATA.keys(): + tmpdir = tempfile.mkdtemp() cert = CertificateGen(course_id) (download_uuid, verify_uuid, download_url) = cert.create_and_upload( - 'John Smith', upload=False, cleanup=False) + 'John Smith', upload=False, copy_to_webroot=True, + cert_web_root=tmpdir, cleanup=True) verify_files = os.listdir( - os.path.join(cert.dir_prefix, S3_VERIFY_PATH, verify_uuid)) + os.path.join(tmpdir, S3_VERIFY_PATH, verify_uuid)) download_files = os.listdir( - os.path.join(cert.dir_prefix, S3_CERT_PATH, download_uuid)) - + os.path.join(tmpdir, S3_CERT_PATH, download_uuid)) # Verify that all files are generated assert_true(set(verify_files) == set(VERIFY_FILES)) assert_true(set(download_files) == set(DOWNLOAD_FILES)) # Verify that the detached signature is valid - pdf = os.path.join(cert.dir_prefix, - S3_CERT_PATH, download_uuid, 'Certificate.pdf') - sig = os.path.join(cert.dir_prefix, - S3_VERIFY_PATH, verify_uuid, 'Certificate.pdf.sig') + pdf = os.path.join( + tmpdir, + S3_CERT_PATH, download_uuid, 'Certificate.pdf' + ) + sig = os.path.join( + tmpdir, + S3_VERIFY_PATH, verify_uuid, 'Certificate.pdf.sig' + ) gpg = gnupg.GPG(gnupghome=settings.CERT_GPG_DIR) with open(sig) as f: @@ -64,8 +55,8 @@ def test_cert_gen(): assert_true(v is not None and v.trust_level >= v.TRUST_FULLY) # Remove files - if os.path.exists(cert.dir_prefix): - shutil.rmtree(cert.dir_prefix) + if os.path.exists(tmpdir): + shutil.rmtree(tmpdir) def test_cert_upload(): @@ -74,12 +65,13 @@ def test_cert_upload(): to S3 and that it can subsequently be downloaded via http """ - skip_if_not_configured() - cert = CertificateGen(settings.CERT_DATA.keys()[0], settings.CERT_AWS_ID, - settings.CERT_AWS_KEY) + cert = CertificateGen( + settings.CERT_DATA.keys()[0], settings.CERT_AWS_ID, + settings.CERT_AWS_KEY + ) (download_uuid, verify_uuid, download_url) = cert.create_and_upload( - 'John Smith') + 'John Smith') r = urllib2.urlopen(download_url) with tempfile.NamedTemporaryFile(delete=True) as f: f.write(r.read()) @@ -90,10 +82,9 @@ def test_cert_names(): Generates certificates for all names in NAMES Deletes them when finished, doesn't upload to S3 """ - skip_if_not_configured() for course_id in settings.CERT_DATA.keys(): for name in NAMES: cert = CertificateGen(course_id) (download_uuid, verify_uuid, download_url) = cert.create_and_upload( - name, upload=False) + name, upload=False)