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)