Source code for shuup.reports.writer

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import six
from babel.dates import format_datetime
from decimal import Decimal
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.http import HttpResponse
from django.template.defaultfilters import floatformat
from django.template.loader import render_to_string
from django.utils.encoding import smart_text
from django.utils.functional import Promise
from django.utils.html import conditional_escape, escape
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from pprint import pformat
from six import BytesIO

from shuup.apps.provides import get_provide_objects
from shuup.core.pricing import TaxfulPrice, TaxlessPrice
from shuup.utils.django_compat import force_text
from shuup.utils.i18n import get_current_babel_locale
from shuup.utils.pdf import render_html_to_pdf

try:
    import openpyxl
except ImportError:
    openpyxl = None


REPORT_WRITERS_MAP = {}


[docs]class ReportWriter(object): content_type = None extension = None inline = False # Implementation-dependent writer_type = "base" def __init__(self): self.title = "" def __unicode__(self): return self.writer_type def __str__(self): return self.writer_type
[docs] def write_heading(self, text): raise NotImplementedError("Error! Not implemented: `ReportWriter` -> `write_heading()`.")
[docs] def write_text(self, text): raise NotImplementedError("Error! Not implemented: `ReportWriter` -> `write_text()`.")
[docs] def write_data_table(self, report, report_data, has_totals=True): raise NotImplementedError("Error! Not implemented: `ReportWriter` -> `write_data_table()`.")
[docs] def write_template(self, template_name, env): raise NotImplementedError("Error! Not implemented: `ReportWriter` -> `write_template()`.")
[docs] def next_page(self): pass
[docs] def get_rendered_output(self): raise NotImplementedError("Error! Not implemented: `ReportWriter` -> `get_rendered_output()`.")
def _render_report(self, report): if not report.rendered: report_data = report.get_data() self.write_heading( "{title} {start} - {end}".format( title=report.title, start=format_datetime(report_data["start"], format="short", locale=get_current_babel_locale()), end=format_datetime(report_data["end"], format="short", locale=get_current_babel_locale()), ) ) report.ensure_texts() self.write_data_table(report, report_data["data"], has_totals=report_data["has_totals"]) return self.get_rendered_output()
[docs] def render_report(self, report, inline=False): """ Renders given report :param report: :type report: :return: :rtype: """ self.inline = inline rendered_output = self._render_report(report) if self.writer_type == "html": output = mark_safe(rendered_output) else: output = mark_safe("<pre>%s</pre>" % escape(rendered_output)) return output
[docs] def get_response(self, report): """ Returns downloadable file response :param report: :type report: :return: :rtype: """ response = HttpResponse(self._render_report(report), content_type=self.content_type) if report.filename_template: response["Content-Disposition"] = "attachment; filename=%s" % self.get_filename(report) return response
[docs] def get_filename(self, report): fmt_data = dict(report.options, time=now().isoformat()) return "%s%s" % ((report.filename_template % fmt_data).replace(":", "_"), self.extension)
[docs]class ExcelReportWriter(ReportWriter): content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" extension = ".xlsx" writer_type = "excel" def __init__(self): super(ExcelReportWriter, self).__init__() self.workbook = openpyxl.Workbook() self.worksheet = self.workbook.active
[docs] def next_page(self): self.worksheet = self.workbook.create_sheet()
def _w(self, content): if content is not None: if isinstance(content, TaxlessPrice) or isinstance(content, TaxfulPrice): content = floatformat(content.amount.value, 2) if isinstance(content, Decimal): content = floatformat(content, 2) if callable(content): content = force_text(content()) if isinstance(content, models.Model): content = force_text(content) return content
[docs] def write_data_table(self, report, report_data, has_totals=True): self.worksheet.append([c["title"] for c in report.schema]) for datum in report_data: datum = report.read_datum(datum) self.worksheet.append([self._w(data) for data in datum]) if has_totals: for datum in report.get_totals(report_data): datum = report.read_datum(datum) self.worksheet.append([self._w(data) for data in datum])
[docs] def write_page_heading(self, text): self.worksheet.append([text]) self.worksheet.append([""])
[docs] def write_heading(self, text): self.worksheet.append([text])
[docs] def write_text(self, text): self.worksheet.append(text)
[docs] def get_rendered_output(self): bio = BytesIO() self.workbook.save(bio) return bio.getvalue()
[docs]class HTMLReportWriter(ReportWriter): content_type = "text/html; charset=UTF-8" extension = ".html" writer_type = "html" INLINE_TEMPLATE = """ <style type="text/css">%(style)s</style> %(body)s """.strip() TEMPLATE = ( """ <html> <head> <meta charset="UTF-8"> <title>%(title)s</title> %(extrahead)s </head>""".strip() + INLINE_TEMPLATE + """</html>""" ) styles = """@page { prince-shrink-to-fit: auto }""".strip() extra_header = "" def __init__(self): super(HTMLReportWriter, self).__init__() self.output = [] def _w_raw(self, content): self.output.append(mark_safe(content)) def _w(self, content): if content is not None: if isinstance(content, TaxlessPrice) or isinstance(content, TaxfulPrice): content = floatformat(content.amount.value, 2) if isinstance(content, Decimal): content = floatformat(content, 2) if isinstance(content, Promise): content = force_text(content) self.output.append(content) def _w_tag(self, tag, content): self._w_raw("<%s>" % tag) self._w(content) self._w_raw("</%s>" % tag)
[docs] def next_page(self): self._w_raw("<hr>")
[docs] def write_data_table(self, report, report_data, has_totals=True): self._w_raw('<table class="table table-striped table-bordered">') self._w_raw("<thead><tr>") for c in report.schema: self._w_tag("th", c["title"]) self._w_raw("</tr></thead>") self._w_raw("<tbody>") for datum in report_data: datum = report.read_datum(datum) self._w_raw("<tr>") for d in datum: self._w_tag("td", d) self._w_raw("</tr>") if has_totals: self._w_raw("<tr>") for d in report.read_datum(report.get_totals(report_data)): self._w_tag("td", d) self._w_raw("</tr>") self._w_raw("</tbody></table>")
[docs] def write_page_heading(self, text): self._w_tag("h1", text)
[docs] def write_heading(self, text): self._w_tag("h2", text)
[docs] def write_text(self, text): self._w(text)
[docs] def write_tag(self, tag, text): self._w_tag(tag, text)
[docs] def get_rendered_output(self): body = "".join(conditional_escape(smart_text(piece)) for piece in self.output) styles = self.styles extrahead = self.extra_header or "" if self.inline: template = self.INLINE_TEMPLATE else: template = self.TEMPLATE html = template % {"title": self.title, "body": body, "style": styles, "extrahead": extrahead} if not self.inline: html = html.encode("UTF-8") return html
[docs] def set_extra(self, extrastring): self.extra_header = extrastring
[docs] def set_style(self, stylestring): self.styles = stylestring
[docs] def write_template(self, template_name, env): data = render_to_string(template_name, env) if isinstance(data, str): data = data.decode("UTF-8") self._w_raw(data)
[docs]class PDFReportWriter(HTMLReportWriter): content_type = "application/pdf" extension = ".pdf" writer_type = "pdf"
[docs] def get_rendered_output(self): html = HTMLReportWriter.get_rendered_output(self) pdf_response = render_html_to_pdf(html) return pdf_response.content
[docs]class JSONReportWriter(ReportWriter): content_type = "application/json" extension = ".json" writer_type = "json" def __init__(self): super(JSONReportWriter, self).__init__() self.data = {}
[docs] def write_data_table(self, report, report_data, has_totals=True): table = { "columns": report.schema, "data": [ dict( (c["key"], force_text(val)) for (c, val) in zip(report.schema, report.read_datum(datum)) # TODO: do not force all text ) for datum in report_data ], } if has_totals: table["totals"] = report.get_totals(report_data) self.data.setdefault("tables", []).append(table)
[docs] def write_heading(self, text): self.data["heading"] = text
[docs] def write_text(self, text): pass
[docs] def get_rendered_output(self): return DjangoJSONEncoder().encode(self.data)
[docs]class PprintReportWriter(JSONReportWriter): content_type = "text/plain" extension = ".txt" writer_type = "pprint"
[docs] def get_rendered_output(self): return pformat(self.data)
[docs]class ReportWriterPopulator(object): """ A class which populates the report writers map. """ report_writers_map = {}
[docs] def populate(self): """ Iterate over all report_writer_populator provides to fill/update the report writer map """ for report_writer_populator_func in get_provide_objects("report_writer_populator"): report_writer_populator_func(self)
[docs] def register(self, writer_name, writer_class): """ Register a report writer for use. If a writer with same name already exists, it will be overwriten. :type writer_name: str :param writer_name: the unique name of the writer :type writer_class: ReportWriter :param writer_class: the report writer class """ self.report_writers_map[writer_name] = writer_class
@property def populated_map(self): """ Returns the populated map. """ return self.report_writers_map
[docs]def get_writer_names(): """ Get the registered writer names. """ return set([k for k, v in six.iteritems(REPORT_WRITERS_MAP) if v])
[docs]def get_writer_instance(writer_name): """ Get a report writer instance by name. :type writer_name: str :param writer_name: the name of the report writer :rtype: ReportWriter """ writer = REPORT_WRITERS_MAP[writer_name]() assert isinstance(writer, ReportWriter) return writer
[docs]def populate_default_writers(writer_populator): """ Populate the default report writers. :type writer_populator: ReportWriterPopulator """ writer_populator.register("html", HTMLReportWriter) writer_populator.register("pdf", PDFReportWriter) writer_populator.register("json", JSONReportWriter) writer_populator.register("pprint", PprintReportWriter) writer_populator.register("html", HTMLReportWriter) if openpyxl: writer_populator.register("excel", ExcelReportWriter)
# populate them populator = ReportWriterPopulator() populator.populate() REPORT_WRITERS_MAP.update(populator.populated_map)