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

Hookable action providers #131

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
2.4.0 (unreleased)
------------------

- #131 Hookable action providers
- #130 Allow direct PDF download of generated report
- #129 Fix template error when the report contains invalidated samples
- #128 Fix AttributeError 'Verificators' on model.verifiers call
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions src/senaite/impress/actions/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">

<!-- PDF Download Action Provider -->
<browser:page
name="impress_download_pdf"
for="*"
class=".providers.DownloadPDF"
permission="zope2.View"
layer="senaite.impress.interfaces.ILayer"
/>

</configure>
49 changes: 49 additions & 0 deletions src/senaite/impress/actions/providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-

import collections

import six

from bika.lims import api
from Products.Five.browser import BrowserView


class CustomAction(BrowserView):
"""Base class for custom actions
"""
def __init__(self, context, request):
self.context = context
self.request = request
self.uids = self.get_uids_from_request()

def get_uids_from_request(self):
"""Returns a list of uids from the request
"""
uids = self.request.get("uids", "")
if isinstance(uids, six.string_types):
uids = uids.split(",")
unique_uids = collections.OrderedDict().fromkeys(uids).keys()
return filter(api.is_uid, unique_uids)


class DownloadPDF(CustomAction):
"""Download Action
"""
def __init__(self, context, request):
super(DownloadPDF, self).__init__(context, request)

def __call__(self):
pdf = self.request.get("pdf")
data = "".join([x for x in pdf.xreadlines()])
return self.download(data)

def download(self, data, mime_type="application/pdf"):
# NOTE: The filename does not matter, because we are downloading
# the PDF in the Ajax handler with createObjectURL
self.request.response.setHeader(
"Content-Disposition", "attachment; filename=report.pdf")
self.request.response.setHeader("Content-Type", mime_type)
self.request.response.setHeader("Content-Length", len(data))
self.request.response.setHeader("Cache-Control", "no-store")
self.request.response.setHeader("Pragma", "no-cache")
self.request.response.write(data)
43 changes: 43 additions & 0 deletions src/senaite/impress/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-

from zope.interface import implementer
from senaite.impress.interfaces import ICustomActionProvider
from bika.lims import api


@implementer(ICustomActionProvider)
class ActionProvider(object):
"""Base class for new action providers
"""
def __init__(self, view, context, request):
self.view = view
self.context = context
self.request = request
# see senaite.impress.interfaces.ICustomFormProvider
self.available = False
self.title = ""
self.name = ""
self.context_url = api.get_url(self.context)
self.url = "{}/{}".format(self.context_url, self.name)
self.modal = False

def get_action_data(self):
return {
"name": self.name,
"title": self.title,
"url": self.url,
"modal": self.modal,
}


class DownloadPDFActionProvider(ActionProvider):
"""Custom action provider to download the report PDF directly
"""
def __init__(self, view, context, request):
super(DownloadPDFActionProvider, self).__init__(view, context, request)
self.available = view.get_allow_pdf_download()
self.title = "PDF"
self.name = "impress_download_pdf"
self.context_url = api.get_url(self.context)
self.url = "{}/{}".format(self.context_url, self.name)
self.modal = False # bypass modal and POST directly to the URL
17 changes: 15 additions & 2 deletions src/senaite/impress/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@

import inspect
import json
from collections import OrderedDict

from bika.lims import _
from bika.lims import api
from collections import OrderedDict
from DateTime import DateTime
from senaite.app.supermodel import SuperModel
from senaite.impress import logger
from senaite.impress.decorators import returns_json
from senaite.impress.decorators import timeit
from senaite.impress.interfaces import ICustomActionProvider
from senaite.impress.interfaces import IPdfReportStorage
from senaite.impress.interfaces import IReportWrapper
from senaite.impress.publishview import PublishView
from zope.component import getAdapters
from zope.component import getMultiAdapter
from zope.interface import implements
from zope.publisher.interfaces import IPublishTraverse
Expand Down Expand Up @@ -161,13 +163,24 @@ def ajax_templates(self):
def ajax_config(self):
"""Returns the default publisher config
"""
custom_actions = []

# Query custom action providers
adapters = getAdapters(
(self, self.context, self.request), ICustomActionProvider)
for name, adapter in adapters:
# skip the adapter if it is not available
if not adapter.available:
continue
custom_actions.append(adapter.get_action_data())

config = {
"format": self.get_default_paperformat(),
"orientation": self.get_default_orientation(),
"template": self.get_default_template(),
"allow_pdf": self.get_allow_pdf_download(),
"allow_save": self.get_allow_publish_save(),
"allow_email": self.get_allow_publish_email(),
"custom_actions": custom_actions,
}
return config

Expand Down
9 changes: 9 additions & 0 deletions src/senaite/impress/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<include package=".analysisrequest" />

<!-- Upgrade Steps -->
<include package=".actions" />
<include package=".upgrades" />

<!-- General Purpose Print View -->
Expand Down Expand Up @@ -132,6 +133,14 @@
factory=".storage.PdfReportStorageAdapter"
permission="zope2.View"/>

<!-- Download PDF Action Adapter -->
<adapter
for="senaite.impress.interfaces.IPublishView
*
zope.publisher.interfaces.browser.IBrowserRequest"
factory=".adapters.DownloadPDFActionProvider"
/>

<!-- Report resource directory -->
<plone:static
directory="templates/reports"
Expand Down
14 changes: 14 additions & 0 deletions src/senaite/impress/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,17 @@ class IReportWrapper(Interface):
def get_metadata(**kw):
"""Generate metadata for the report
"""


class ICustomActionProvider(Interface):
"""Provide additional action buttons in report preview
"""
available = Attribute("Boolean to control if the action is available")
title = Attribute("Button title of the action")
name = Attribute("Button name of the action")
url = Attribute("Form action or modal URL")
modal = Attribute("Boolean to control if the action opens a new modal")

def get_action_data():
"""Returns the known attributes to the ReactJS component
"""
2 changes: 1 addition & 1 deletion src/senaite/impress/static/bundles/senaite.impress.js

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions webpack/app/api.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,7 @@ class PublishAPI
return @get_json("save_reports", options)


print_pdf: (options) ->
###
* Send an async request to the server and download the file
###

create_pdf: (options) ->
# wrap all options into form data
formData = new FormData()
formData.set("download", "1")
Expand All @@ -145,7 +141,14 @@ class PublishAPI
# submit the POST and display the PDF in a new window
return fetch(request).then (response) ->
return response.blob()
.then (blob) ->


print_pdf: (options) ->
###
* Send an async request to the server and download the file
###

@create_pdf(options).then (blob) ->
# open the PDF in a separate window
url= window.URL.createObjectURL(blob)
window.open(url, "_blank")
Expand Down
2 changes: 1 addition & 1 deletion webpack/app/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Button extends React.Component {

render() {
return (
<button uid={this.props.uid} name={this.props.name} onClick={this.props.onClick} className={this.props.className}>
<button uid={this.props.uid} name={this.props.name} url={this.props.url} onClick={this.props.onClick} className={this.props.className}>
{this.props.title}
</button>
);
Expand Down
16 changes: 16 additions & 0 deletions webpack/app/components/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

class Modal extends React.Component {

constructor(props) {
super(props);
}

render() {
return (
<div id={this.props.id} className="modal fade" tabindex="-1"></div>
);
}
}

export default Modal;
Loading