Passed
Push — 2.x ( da17b1...a43b53 )
by Jordi
06:56
created

bika.lims.browser.stickers.Sticker.get_back_url()   A

Complexity

Conditions 3

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 15
rs 9.8
c 0
b 0
f 0
cc 3
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import os
22
import os.path
23
import tempfile
24
import traceback
25
26
from bika.lims import api
27
from bika.lims import bikaMessageFactory as _
28
from bika.lims import logger
29
from bika.lims.browser import BrowserView
30
from bika.lims.interfaces import IGetStickerTemplates
31
from bika.lims.utils import createPdf
32
from bika.lims.utils import to_int
33
from bika.lims.vocabularies import getStickerTemplates
34
from plone.resource.utils import queryResourceDirectory
35
from Products.CMFPlone.utils import safe_unicode
36
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
37
from senaite.app.supermodel import SuperModel
38
from zope.component import getAdapters
39
from zope.component.interfaces import ComponentLookupError
40
41
42
class Sticker(BrowserView):
43
    """Invoked via URL on an object or list of objects from the types
44
       AnalysisRequest, Sample or ReferenceSample.
45
46
       Renders a preview for the objects, a control to allow the user to
47
       select the sticker template to be invoked and print.
48
49
       In order to create a sticker inside an Add-on you have to create a
50
       directory inside the resource directory
51
52
       This defines the resource folder to look for:
53
54
       - path: addon/stickers/configure.zcml
55
           ...
56
           **Defining stickers for samples and partitions
57
           <plone:static
58
             directory="templates"
59
             type="stickers"
60
             name="ADDON stickers" />
61
           ...
62
63
       This is how to add general stickers for samples:
64
65
       - addon/stickers/templates/
66
67
           -- code_39_40x20mm.{css,pt}
68
           -- other_{sample,ar,partition}_stickers_...
69
70
       This is the way to create specific sticker for a content type.
71
72
       Note that in this case the directory '/worksheet' should contain the
73
       sticker templates for worksheet objects.
74
75
       - addon/stickers/templates/worksheet
76
           -- code_...mm.{css,pt}
77
           -- other_worksheet_stickers_...
78
    """
79
    template = ViewPageTemplateFile("templates/stickers_preview.pt")
80
81
    def __init__(self, context, request):
82
        super(Sticker, self).__init__(context, request)
83
        self.context = context
84
        self.request = request
85
        self.current_item = None
86
87
    def __call__(self):
88
        # Need to generate a PDF with the stickers?
89
        if self.request.form.get("pdf", "0") == "1":
90
            response = self.request.response
91
            response.setHeader("Content-type", "application/pdf")
92
            response.setHeader("Content-Disposition", "inline")
93
            response.setHeader("filename", "sticker.pdf")
94
            pdfstream = self.pdf_from_post()
95
            return pdfstream
96
97
        # If filter by type is given in the request, only the templates under
98
        # the path with the type name will be given as vocabulary.
99
        # Example: If filter_by_type=='worksheet', only *.pt files under a
100
        # folder with filter_by_type as name will be displayed.
101
        self.filter_by_type = self.request.get("filter_by_type", False)
102
103
        self.items = self.get_items()
104
        if not self.items:
105
            logger.warning(
106
                "Cannot print stickers: no items specified in request")
107
            self.request.response.redirect(self.context.absolute_url())
108
            return
109
110
        return self.template()
111
112
    def get_back_url(self):
113
        """Calculate the Back URL
114
        """
115
        url = api.get_url(self.context)
116
        portal_type = api.get_portal_type(self.context)
117
        redirect_contexts = ['Client', 'AnalysisRequest', 'Samples', 'Batch']
118
        if portal_type not in redirect_contexts:
119
            parent = api.get_parent(self.context)
120
            url = api.get_url(parent)
121
        # redirect to direct results entry
122
        setup = api.get_setup()
123
        if setup.getImmediateResultsEntry():
124
            url = "{}/multi_results?uids={}".format(
125
                url, ",".join(self.get_uids()))
126
        return url
127
128
    def get_items(self):
129
        """Returns a list of SuperModel items
130
        """
131
        uids = self.get_uids()
132
        if not uids:
133
            return [SuperModel(self.context)]
134
        items = map(lambda uid: SuperModel(uid), uids)
135
        return self._resolve_number_of_copies(items)
136
137
    def get_uids(self):
138
        """Parse the UIDs from the request `items` parameter
139
        """
140
        return filter(None, self.request.get("items", "").split(","))
141
142
    def getAvailableTemplates(self):
143
        """Returns an array with the templates of stickers available.
144
145
        Each array item is a dictionary with the following structure:
146
147
            {'id': <template_id>,
148
            'title': <teamplate_title>,
149
            'selected: True/False'}
150
        """
151
        # Getting adapters for current context. those adapters will return
152
        # the desired sticker templates for the current context:
153
        try:
154
            adapters = getAdapters((self.context, ), IGetStickerTemplates)
155
        except ComponentLookupError:
156
            logger.info("No IGetStickerTemplates adapters found.")
157
            adapters = None
158
        templates = []
159
        if adapters is not None:
160
            # Gather all templates
161
            for name, adapter in adapters:
162
                templates += adapter(self.request)
163
        if templates:
164
            return templates
165
        # If there are no adapters, get all sticker templates in the system
166
        seltemplate = self.getSelectedTemplate()
167
        for temp in getStickerTemplates(filter_by_type=self.filter_by_type):
168
            out = temp
169
            out["selected"] = temp.get("id", "") == seltemplate
170
            templates.append(out)
171
        return templates
172
173
    def getSelectedTemplate(self, default="Code_39_40x20mm.pt"):
174
        """Returns the id of the sticker template selected.
175
176
        If no specific template found in the request (parameter template),
177
        returns the default template set in Setup > Stickers.
178
179
        If the template doesn't exist, uses the default template.
180
181
        If no template selected but size param, get the sticker template set as
182
        default in Bika Setup for the size set.
183
        """
184
        # Default sticker
185
        bs_template = self.context.bika_setup.getAutoStickerTemplate()
186
        size = self.request.get("size", "")
187
188
        if self.filter_by_type:
189
            templates = getStickerTemplates(filter_by_type=self.filter_by_type)
190
            # Get the first sticker
191
            bs_template = templates[0].get("id", "") if templates else ""
192
        elif size == "small":
193
            bs_template = self.context.bika_setup.getSmallStickerTemplate()
194
        elif size == "large":
195
            bs_template = self.context.bika_setup.getLargeStickerTemplate()
196
        rq_template = self.request.get("template", bs_template)
197
        # Check if the template exists. If not, fallback to default's
198
        # 'prefix' is also the resource folder's name
199
        prefix = ""
200
        templates_dir = ""
201
        if rq_template.find(":") >= 0:
202
            prefix, rq_template = rq_template.split(":")
203
            templates_dir = self._getStickersTemplatesDirectory(prefix)
204
        else:
205
            this_dir = os.path.dirname(os.path.abspath(__file__))
206
            templates_dir = os.path.join(this_dir, "templates/stickers/")
207
            if self.filter_by_type:
208
                templates_dir = templates_dir + "/" + self.filter_by_type
209
        if not os.path.isfile(os.path.join(templates_dir, rq_template)):
210
            rq_template = default
211
        return "%s:%s" % (prefix, rq_template) if prefix else rq_template
212
213
    def getSelectedTemplateCSS(self):
214
        """Looks for the CSS file from the selected template and return its
215
           contents.
216
217
        If the selected template is default.pt, looks for a file named
218
        default.css in the stickers path and return its contents. If no CSS
219
        file found, retrns an empty string
220
        """
221
        template = self.getSelectedTemplate()
222
        # Look for the CSS
223
        content = ""
224
        if template.find(":") >= 0:
225
            # A template from another add-on
226
            prefix, template = template.split(":")
227
            templates_dir = self._getStickersTemplatesDirectory(prefix)
228
            css = "{0}.css".format(template[:-3])
229
            if css in os.listdir(templates_dir):
230
                path = "%s/%s.css" % (templates_dir, template[:-3])
231
                if os.path.isfile(path):
232
                    with open(path, "r") as content_file:
233
                        content = content_file.read()
234
        else:
235
            this_dir = os.path.dirname(os.path.abspath(__file__))
236
            templates_dir = os.path.join(this_dir, "templates/stickers/")
237
            # Only use the directory asked in "filter_by_type"
238
            if self.filter_by_type:
239
                templates_dir = templates_dir + "/" + self.filter_by_type
240
            path = "%s/%s.css" % (templates_dir, template[:-3])
241
            if os.path.isfile(path):
242
                with open(path, "r") as content_file:
243
                    content = content_file.read()
244
        return content
245
246
    def renderItem(self, item):
247
        """Renders the next available sticker.
248
249
        Uses the template specified in the request ('template' parameter) by
250
        default. If no template defined in the request, uses the default
251
        template set by default in Setup > Stickers.
252
253
        If the template specified doesn't exist, uses the default bika.lims'
254
        Code_128_1x48mm.pt template (was sticker_small.pt).
255
        """
256
        self.current_item = item
257
        templates_dir = "templates/stickers"
258
        embedt = self.getSelectedTemplate()
259
        if embedt.find(":") >= 0:
260
            prefix, embedt = embedt.split(":")
261
            templates_dir = self._getStickersTemplatesDirectory(prefix)
262
        elif self.filter_by_type:
263
            templates_dir = "/".join([templates_dir, self.filter_by_type])
264
        fullpath = os.path.join(templates_dir, embedt)
265
266
        try:
267
            embed = ViewPageTemplateFile(fullpath)
268
            return embed(self, item=item)
269
        except Exception:
270
            exc = traceback.format_exc()
271
            msg = "<div class='error'>{} - {} '{}':<pre>{}</pre></div>".format(
272
                item.id, _("Failed to load sticker"), embedt, exc)
273
            return msg
274
275
    def getItemsURL(self):
276
        """Used in stickers_preview.pt
277
        """
278
        req_items = self.get_uids()
279
        req_items = req_items or [api.get_uid(self.context)]
280
        req = "{}?items={}".format(self.request.URL, ",".join(req_items))
281
        return req
282
283
    def _getStickersTemplatesDirectory(self, resource_name):
284
        """Returns the paths for the directory containing the css and pt files
285
        for the stickers deppending on the filter_by_type.
286
287
        :param resource_name: The name of the resource folder.
288
        :type resource_name: string
289
        :returns: a string as a path
290
        """
291
        templates_dir =\
292
            queryResourceDirectory("stickers", resource_name).directory
293
        if self.filter_by_type:
294
            templates_dir = templates_dir + "/" + self.filter_by_type
295
        return templates_dir
296
297
    def pdf_from_post(self):
298
        """Returns a pdf stream with the stickers
299
        """
300
        html = self.request.form.get("html")
301
        style = self.request.form.get("style")
302
        reporthtml = "<html><head>{0}</head><body>{1}</body></html>"
303
        reporthtml = reporthtml.format(style, html)
304
        reporthtml = safe_unicode(reporthtml).encode("utf-8")
305
        pdf_fn = tempfile.mktemp(suffix=".pdf")
306
        pdf_file = createPdf(htmlreport=reporthtml, outfile=pdf_fn)
307
        return pdf_file
308
309
    def _resolve_number_of_copies(self, items):
310
        """For the given objects generate as many copies as the desired number
311
        of stickers.
312
313
        :param items: list of objects whose stickers are going to be previewed.
314
        :type items: list
315
        :returns: list containing n copies of each object in the items list
316
        :rtype: list
317
        """
318
        copied_items = []
319
        for obj in items:
320
            for copy in range(self.get_copies_count()):
321
                copied_items.append(obj)
322
        return copied_items
323
324
    def get_copies_count(self):
325
        """Return the copies_count number request parameter
326
327
        :returns: the number of copies for each sticker as stated
328
        in the request
329
        :rtype: int
330
        """
331
        setup = api.get_setup()
332
        default_num = setup.getDefaultNumberOfCopies()
333
        request_num = self.request.form.get("copies_count")
334
        return to_int(request_num, default_num)
335