Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements on the freeform-answers report #17

Merged
merged 3 commits into from Aug 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 70 additions & 5 deletions eoc_journal/eoc_journal.py
Expand Up @@ -4,16 +4,20 @@

from collections import OrderedDict
from io import BytesIO
from urlparse import urljoin

import webob
from django.conf import settings

from lxml import html
from lxml.html.clean import clean_html

from reportlab.lib import pagesizes
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.colors import Color
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer

from problem_builder.models import Answer
from reportlab.platypus.flowables import HRFlowable
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String, List
from xblock.fragment import Fragment
Expand All @@ -22,6 +26,7 @@

from .api_client import ApiClient, calculate_engagement_score
from .course_blocks_api import CourseBlocksApiClient
from .pdf_generator import get_style_sheet
from .utils import _, normalize_id

try:
Expand Down Expand Up @@ -85,6 +90,13 @@ class EOCJournalXBlock(StudioEditableXBlockMixin, XBlock):
list_values_provider=provide_pb_answer_list,
)

pdf_report_title = String(
display_name=_("PDF Title"),
help=_("Title of the PDF report. Leave blank to use the course title."),
default=None,
scope=Scope.content,
)

pdf_report_link_heading = String(
display_name=_("PDF Report Link heading"),
help=_("The heading text to display above the link for downloading the PDF Report."),
Expand Down Expand Up @@ -120,15 +132,27 @@ class EOCJournalXBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.content,
)

custom_font = String(
display_name=_("Default Font"),
help=_("Studio static URL to a custom font file to be used for PDF report. "
"Example: \"/static/myfont.ttf\". Leave empty to use default fonts. "
"You can upload custom TTF font files from the Content - Files "
"& Uploads page."),
default=None,
scope=Scope.settings,
)

editable_fields = (
'display_name',
'key_takeaways_pdf',
'selected_pb_answer_blocks',
'pdf_report_title',
'pdf_report_link_heading',
'pdf_report_link_text',
'display_metrics_section',
'display_key_takeaways_section',
'display_answers',
'custom_font',
)

def student_view(self, context=None):
Expand Down Expand Up @@ -175,11 +199,14 @@ def serve_pdf(self, request, _suffix):
"""
Builds and serves a PDF document containing user's freeform answers.
"""
styles = getSampleStyleSheet()
font_path = self._expand_static_url(self.custom_font, absolute=True) if self.custom_font else None
styles = get_style_sheet(font_url=font_path)
pdf_buffer = BytesIO()
document = SimpleDocTemplate(pdf_buffer, pagesize=pagesizes.letter, title=_("Report"))

report_header_name = self.pdf_report_title or self._get_course_name()
document = SimpleDocTemplate(pdf_buffer, pagesize=pagesizes.letter, title=report_header_name)
story = [
Paragraph(self.display_name, styles["Title"]),
Paragraph(report_header_name, styles["Title"]),
]

answer_sections = self.list_user_pb_answers_by_section()
Expand All @@ -189,6 +216,7 @@ def serve_pdf(self, request, _suffix):
for question in section["questions"]:
story.append(Paragraph(question["question"], styles["h2"]))
story.append(Paragraph(question["answer"], styles["Normal"]))
story.append(HRFlowable(color=Color(0, 0, 0, 0.1), width='100%', spaceBefore=5, spaceAfter=10))

document.build(story)
pdf_buffer.seek(0)
Expand Down Expand Up @@ -301,6 +329,39 @@ def _get_current_anonymous_user_id(self):
"""
return self.runtime.anonymous_student_id

def _get_course_name(self):
"""
Get the name of the current course, for the downloadable report.
"""
try:
course_key = self.scope_ids.usage_id.course_key
except AttributeError:
return '' # We are not in an edX runtime

try:
course_root_key = course_key.make_usage_key('course', 'course')
return self.runtime.get_block(course_root_key).display_name
# ItemNotFoundError most likely, but we can't import that exception in non-edX environments
except Exception: # pylint: disable=W0703
# We may be on old mongo:
try:
course_root_key = course_key.make_usage_key('course', course_key.run)
return self.runtime.get_block(course_root_key).display_name
except Exception: # pylint: disable=W0703
return ''

@staticmethod
def _make_url_absolute(url):
"""
This method will turn make relative urls absolute. It's helpfull in
some cases where some functions treat a varible as a path and url in
the same time
"""
lms_base = settings.ENV_TOKENS.get('LMS_BASE')
scheme = 'https' if settings.HTTPS == 'on' else 'http'
lms_base = '{}://{}'.format(scheme, lms_base)
return urljoin(lms_base, url)

def get_progress_metrics(self):
"""
Fetches and returns dict with progress metrics for the current user
Expand Down Expand Up @@ -403,7 +464,7 @@ def _fetch_pb_answer_blocks(self, all_blocks=False):
)
return response

def _expand_static_url(self, url):
def _expand_static_url(self, url, absolute=False):
"""
This is required to make URLs like '/static/takeaways.pdf' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
Expand All @@ -420,4 +481,8 @@ def _expand_static_url(self, url):
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass

if absolute:
url = self._make_url_absolute(url)

return url
185 changes: 185 additions & 0 deletions eoc_journal/pdf_generator.py
@@ -0,0 +1,185 @@
"""
Utils around Reportlab customizations
"""
import logging

from reportlab.lib.enums import TA_CENTER
from reportlab.lib.fonts import tt2ps
from reportlab.lib.styles import (
getSampleStyleSheet,
StyleSheet1,
ParagraphStyle,
)

import reportlab
import reportlab.rl_config
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont, TTFError

log = logging.getLogger(__name__)


def get_style_sheet(font_url=None):
"""Returns a custom stylesheet object"""
default_style_sheet = getSampleStyleSheet()

if not font_url:
return default_style_sheet

stylesheet = StyleSheet1()
font_name = 'customFont'

try:
font = TTFont(font_name, font_url)
except TTFError:
log.warning(u'Cannot load %s', font_url)
return default_style_sheet

reportlab.rl_config.warnOnMissingFontGlyphs = 0
pdfmetrics.registerFont(font)

font_name_bold = tt2ps(font_name, 1, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I didn't know about the tt2ps function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither! It's saved us a lot of bad code.

font_name_italic = tt2ps(font_name, 0, 1)
font_name_bold_italic = tt2ps(font_name, 1, 1)

stylesheet.add(ParagraphStyle(
name='Normal',
fontName=font_name,
fontSize=10,
leading=12
))

stylesheet.add(ParagraphStyle(
name='BodyText',
parent=stylesheet['Normal'],
spaceBefore=6
))
stylesheet.add(ParagraphStyle(
name='Italic',
parent=stylesheet['BodyText'],
fontName=font_name_italic
))

stylesheet.add(
ParagraphStyle(
name='Heading1',
parent=stylesheet['Normal'],
fontName=font_name_bold,
fontSize=18,
leading=22,
spaceAfter=6
),
alias='h1'
)

stylesheet.add(
ParagraphStyle(
name='Title',
parent=stylesheet['Normal'],
fontName=font_name_bold,
fontSize=22,
leading=22,
alignment=TA_CENTER,
spaceAfter=6
),
alias='title'
)

stylesheet.add(
ParagraphStyle(
name='Heading2',
parent=stylesheet['Normal'],
fontName=font_name_bold,
fontSize=14,
leading=18,
spaceBefore=12,
spaceAfter=6
),
alias='h2'
)

stylesheet.add(
ParagraphStyle(
name='Heading3',
parent=stylesheet['Normal'],
fontName=font_name_bold_italic,
fontSize=12,
leading=14,
spaceBefore=12,
spaceAfter=6
),
alias='h3'
)

stylesheet.add(ParagraphStyle(
name='Heading4',
parent=stylesheet['Normal'],
fontName=font_name_bold_italic,
fontSize=10,
leading=12,
spaceBefore=10,
spaceAfter=4
),
alias='h4'
)

stylesheet.add(ParagraphStyle(
name='Heading5',
parent=stylesheet['Normal'],
fontName=font_name_bold,
fontSize=9,
leading=10.8,
spaceBefore=8,
spaceAfter=4
),
alias='h5'
)

stylesheet.add(
ParagraphStyle(
name='Heading6',
parent=stylesheet['Normal'],
fontName=font_name_bold,
fontSize=7,
leading=8.4,
spaceBefore=6,
spaceAfter=2
),
alias='h6'
)

stylesheet.add(
ParagraphStyle(
name='Bullet',
parent=stylesheet['Normal'],
firstLineIndent=0,
spaceBefore=3
),
alias='bu'
)

stylesheet.add(
ParagraphStyle(
name='Definition',
parent=stylesheet['Normal'],
firstLineIndent=0,
leftIndent=36,
bulletIndent=0,
spaceBefore=6,
bulletFontName=font_name_bold_italic
),
alias='df'
)

stylesheet.add(
ParagraphStyle(
name='Code',
parent=stylesheet['Normal'],
fontName='Courier',
fontSize=8,
leading=8.8,
firstLineIndent=0,
leftIndent=36
))

return stylesheet
2 changes: 1 addition & 1 deletion pylintrc
Expand Up @@ -12,4 +12,4 @@ disable=
locally-disabled

[OPTIONS]
good-names=_,loader
good-names=_,loader,log