Skip to content

Commit

Permalink
Merge pull request #424 from Deep-Chill/issue-421
Browse files Browse the repository at this point in the history
Allow downloading assessment scorecards as PPTX (#421)
  • Loading branch information
smirolo authored Dec 19, 2023
2 parents c66e472 + f29e3d7 commit 4950dce
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 2 deletions.
4 changes: 3 additions & 1 deletion djaopsp/urls/views/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from ...views.assess import (AssessPracticesXLSXView, AssessPracticesView,
AssessRedirectView, ImprovePracticesView, ImproveRedirectView,
TrackMetricsView)
TrackMetricsView, AssessPracticesPPTXView)
from ...views.downloads import ImproveContentPDFView
from ...views.scorecard import (ScorecardIndexView, ScorecardHistoryView,
ScorecardRedirectView, DataValuesView)
Expand All @@ -27,6 +27,8 @@
AssessPracticesXLSXView.as_view(), name='assess_download_segment'),
path('assess/<slug:sample>/download/',
AssessPracticesXLSXView.as_view(), name='assess_download'),
path('assess/<slug:sample>/download/pptx/',
AssessPracticesPPTXView.as_view(), name='assess_download_pptx'),
path('assess/<slug:sample>/<path:path>/',
AssessPracticesView.as_view(), name='assess_practices'),
path('assess/<slug:sample>/',
Expand Down
229 changes: 228 additions & 1 deletion djaopsp/views/assess.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
from __future__ import unicode_literals

import datetime, json, logging
from io import BytesIO

from deployutils.apps.django.templatetags.deployutils_prefixtags import (
site_url)
from deployutils.helpers import update_context_urls
from django.core.files.storage import get_storage_class
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from django.views.generic.base import (ContextMixin, RedirectView,
TemplateResponseMixin, TemplateView)
from django.template.defaultfilters import slugify
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.util import Inches, Pt

from survey.helpers import get_extra
from survey.models import Choice, EditableFilter
from survey.queries import datetime_or_now
Expand Down Expand Up @@ -452,3 +458,224 @@ def format_row(self, entry, key=None):
def get_filename(self):
return datetime_or_now().strftime("%s-%s-%%Y%%m%%d.xlsx" % (
self.sample.account.slug, self.campaign.slug))


class AssessPracticesPPTXView(AssessmentContentMixin, ListView):
"""
Allows downloading an assessment as a PPTX
"""

def __init__(self):
super().__init__()
self.prs = Presentation()
self.template_config = {
"background_color": [255, 255, 255],
"title_font": "Arial",
"title_font_size": 18,
"title_font_color": [42, 87, 141],
"body_font": "Calibri",
"body_font_size": 12,
"left": 1,
"top": 2,
"width": 8,
"height": 0.5,
}
self.update_template_settings()
self.current_slide = None
self.title_hierarchy = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None}
self.basename = 'practices'

def update_template_settings(self):
self.left = Inches(self.template_config["left"])
self.top = Inches(self.template_config["top"])
self.width = Inches(self.template_config["width"])
self.height = Inches(self.template_config["height"])

def apply_background(self, slide):
bg_color = self.template_config.get("background_color")
if bg_color:
slide.background.fill.solid()
slide.background.fill.fore_color.rgb = RGBColor(*bg_color)

def format_slide_title(self, title_shape):
title_shape.text_frame.paragraphs[0].font.name = self.template_config["title_font"]
title_shape.text_frame.paragraphs[0].font.size = Pt(self.template_config["title_font_size"])
title_shape.text_frame.paragraphs[0].font.bold = True
title_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*self.template_config["title_font_color"])

def format_entry_content(self, entry):
content = []
try:
default_unit_slug = entry['default_unit'].slug
except AttributeError:
default_unit_slug = entry.get('default_unit', {}).get('slug', "")

answers = entry.get('answers', [])
primary_assessed = None
primary_planned = None
points = None
comments = ""

for answer in answers:
try:
unit_slug = answer['unit'].slug
except AttributeError:
unit_slug = ""

measured = answer.get('measured')
if unit_slug == default_unit_slug:
primary_assessed = measured
elif unit_slug == 'freetext':
comments = measured
elif unit_slug == 'points':
points = float(measured) if measured else None

planned = entry.get('planned', [])
for plan in planned:
try:
unit_slug = plan['unit'].slug
except AttributeError:
unit_slug = ""

if unit_slug == default_unit_slug:
primary_planned = plan.get('measured')

title = entry['title']
content.append(f"Title: {title}")
opportunity = entry.get('opportunity')
rates = entry.get('rate', {})
nb_respondents = entry.get('nb_respondents', {})
extra = entry.get('extra')
avg_value = 0

if primary_assessed is not None:
content.append(f"Assessed: {primary_assessed}")
if primary_planned is not None:
content.append(f"Planned: {primary_planned}")
if points is not None:
content.append(f"Points: {points}")
if comments:
content.append(f"Comments: {comments}")
if opportunity:
content.append(f"Opportunity: {opportunity}")
if nb_respondents:
content.append(f"Number of respondents: {nb_respondents}")
if rates:
for choice, value in rates.items():
content.append(f"{choice}: {value}")
if extra:
intrinsic_values = extra.get('intrinsic_values')
if intrinsic_values:
environmental_value = intrinsic_values.get('environmental', 0)
business_value = intrinsic_values.get('business', 0)
profitability = intrinsic_values.get('profitability', 0)
implementation_ease = intrinsic_values.get(
'implementation_ease', 0)
avg_value = (environmental_value + business_value +
profitability + implementation_ease) // 4
if avg_value:
content.append(f"Environmental value: {environmental_value}")
content.append(f"Business value: {business_value}")
content.append(f"Profitability: {profitability}")
content.append(f"Implementation Ease: {implementation_ease}")
content.append(f"Average Value: {avg_value}")

return "\n".join(content)


def add_new_slide(self, presentation, title):
self.top = Inches(self.template_config["top"])
slide_layout = presentation.slide_layouts[5]
slide = presentation.slides.add_slide(slide_layout)
self.apply_background(slide)
title_shape = slide.shapes.title
title_shape.text = title
self.format_slide_title(title_shape)
return slide

def add_content_to_slide(self, slide, entry):
textbox = slide.shapes.add_textbox(self.left, self.top, self.width, self.height)
text_frame = textbox.text_frame
text_frame.word_wrap = True
content = self.format_entry_content(entry)

for line in content.split("\n"):
p = text_frame.add_paragraph()

if ": " in line:
prefix, rest = line.split(": ", 1)
self.add_text_run(p, prefix + ":", bold=True)
else:
rest = line

self.add_text_run(p, rest)

def add_text_run(self, paragraph, text, bold=False):
run = paragraph.add_run()
run.text = text
run.font.bold = bold
run.font.name = self.template_config["body_font"]
run.font.size = Pt(self.template_config["body_font_size"])

def add_extra_content_to_title_slide(self, slide, extra_values):
for key, value in extra_values.items():
textbox = slide.shapes.add_textbox(self.left, self.top, self.width, self.height)
text_frame = textbox.text_frame
text_frame.word_wrap = True

if key and value:
p = text_frame.add_paragraph()
p.text = f"{key}: {value}"
for run in p.runs:
run.font.name = self.template_config["body_font"]
run.font.size = Pt(self.template_config["body_font_size"])

self.top += self.height

def get(self, request, *args, **kwargs):
self.current_slide = None
queryset = self.get_queryset()

for entry in queryset:
indent_level = entry['indent']
title = entry['title']

if indent_level > 1:
self.title_hierarchy[indent_level] = title
for higher_level in range(indent_level + 1, 6):
self.title_hierarchy[higher_level] = None

if 'answers' in entry:
title_parts = [self.title_hierarchy[level] for level in range(1, indent_level) if self.title_hierarchy[level] is not None]
composed_title = ' - '.join(title_parts)
self.current_slide = self.add_new_slide(self.prs, composed_title)
self.add_content_to_slide(self.current_slide, entry)

elif indent_level == 0:
self.current_slide = self.add_new_slide(self.prs, title)
extra_values = {
'Campaign': self.sample.campaign.title,
'Created at': self.sample.created_at.date(),
'Campaign Creator': self.sample.account.full_name,
'Normalized score': entry.get('normalized_score'),
}
self.add_extra_content_to_title_slide(self.current_slide, extra_values)

elif indent_level == 1:
self.current_slide = self.add_new_slide(self.prs, title)
self.title_hierarchy[1] = title
for higher_level in range(2, 6):
self.title_hierarchy[higher_level] = None
ppt_io = BytesIO()
self.prs.save(ppt_io)
ppt_io.seek(0)
filename = self.get_filename()
response = HttpResponse(
ppt_io,
content_type=self.content_type)
response['Content-Disposition'] = f'attachment; filename={filename}'
return response

def get_filename(self):
return datetime_or_now().strftime("%s-%s-%%Y%%m%%d.pptx" % (
self.sample.account.slug, self.campaign.slug))

0 comments on commit 4950dce

Please sign in to comment.