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

Dynamic Analysis Specifications #1492

Merged
merged 29 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
39b7c8a
Implemented Types and added Setup Routines
ramonski Dec 14, 2019
e770254
Added file validator
ramonski Dec 14, 2019
d68f0ab
Added icon in specs widget when a dynamic spec was found
ramonski Dec 14, 2019
58dd539
Fixed grouping method
ramonski Dec 14, 2019
bae417e
Added dynamic analysisspec adapter
ramonski Dec 14, 2019
cf82875
Better docstrings
ramonski Dec 15, 2019
be78dcd
Merge branch 'master' into dynamic-analysisspecs
ramonski Dec 15, 2019
db6a62f
Removed duplicate line
ramonski Dec 15, 2019
f965e7e
Minor change
ramonski Dec 17, 2019
3884475
Return immediately if now services were selected
ramonski Dec 17, 2019
ef77046
Lookup range keys from the schema
ramonski Dec 18, 2019
56fe0e4
Make DynamicSpecs field non-required
xispa Dec 19, 2019
4e30368
Subfield validators from other add-ons are dismissed, as well as thei…
xispa Dec 19, 2019
b7538e5
Better formatting of specs validators messages
xispa Dec 19, 2019
6686b20
Merge branch 'master' into dynamic-analysisspecs
ramonski Dec 23, 2019
e04f2c4
Merge branch 'master' into dynamic-analysisspecs
ramonski Jan 14, 2020
86543f3
Merge branch 'master' into dynamic-analysisspecs
ramonski Jan 15, 2020
8df7ddf
Merge branch 'master' into dynamic-analysisspecs
xispa Jan 21, 2020
9074529
Merge branch 'master' into dynamic-analysisspecs
ramonski Jan 25, 2020
994fd83
Fixed specification matching
ramonski Jan 25, 2020
12439f3
Added doctest
ramonski Jan 25, 2020
069a354
Try to convert the markdown table
ramonski Jan 25, 2020
cc18f43
Use RST Table format
ramonski Jan 25, 2020
8e9476b
Display Dynamic Analysis Spec link at legacy's controlpanel
xispa Jan 26, 2020
d65f27b
misspelling
xispa Jan 26, 2020
025947a
Merge branch 'master' of github.com:senaite/senaite.core into dynamic…
xispa Jan 28, 2020
63d0e5a
Add a viewlet for when a spec has a dynamic spec assigned
xispa Jan 28, 2020
14c81ad
Add column Dynamic Specification in Analysis Specs listing
xispa Jan 28, 2020
ab15d24
Imports cleanup
xispa Jan 28, 2020
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
8 changes: 8 additions & 0 deletions bika/lims/adapters/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
xmlns:monkey="http://namespaces.plone.org/monkey"
i18n_domain="senaite.core">

<!-- Dynamic Results Range Adapter
Looked up in `abstractroutineanalysis.getResultsRange` -->
<adapter
for="bika.lims.interfaces.IRoutineAnalysis"
factory=".dynamicresultsrange.DynamicResultsRange"
provides="bika.lims.interfaces.IDynamicResultsRange"
/>

<adapter
for="zope.interface.Interface
zope.publisher.interfaces.browser.IBrowserRequest"
Expand Down
138 changes: 138 additions & 0 deletions bika/lims/adapters/dynamicresultsrange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-

from bika.lims import api
from bika.lims.interfaces import IDynamicResultsRange
from zope.interface import implementer

marker = object()

DEFAULT_RANGE_KEYS = [
"min",
"warn_min",
"min_operator",
"minpanic",
"max",
"warn_max",
"max",
"maxpanic",
"error",
]


@implementer(IDynamicResultsRange)
class DynamicResultsRange(object):
"""Default Dynamic Results Range Adapter
"""

def __init__(self, analysis):
self.analysis = analysis
self.analysisrequest = analysis.getRequest()
self.specification = self.analysisrequest.getSpecification()
self.dynamicspec = None
if self.specification:
self.dynamicspec = self.specification.getDynamicAnalysisSpec()

@property
def keyword(self):
"""Analysis Keyword
"""
return self.analysis.getKeyword()

@property
def range_keys(self):
"""The keys of the result range dict
"""
if not self.specification:
return DEFAULT_RANGE_KEYS
# return the subfields of the specification
return self.specification.getField("ResultsRange").subfields

def convert(self, value):
# convert referenced UIDs to the Title
if api.is_uid(value):
obj = api.get_object_by_uid(value)
return api.get_title(obj)
return value

def get_match_data(self):
"""Returns a fieldname -> value mapping of context data

The fieldnames are selected from the column names of the dynamic
specifications file. E.g. the column "Method" of teh specifications
file will lookup the value (title) of the Analysis and added to the
mapping like this: `{"Method": "Method-1"}`.

:returns: fieldname -> value mapping
:rtype: dict
"""
data = {}

# Lookup the column names on the Analysis and the Analysis Request
for column in self.dynamicspec.get_header():
an_value = getattr(self.analysis, column, marker)
ar_value = getattr(self.analysisrequest, column, marker)
if an_value is not marker:
data[column] = self.convert(an_value)
elif ar_value is not marker:
data[column] = self.convert(ar_value)

return data

def get_results_range(self):
"""Return the dynamic results range

The returning dicitionary should containe at least the `min` and `max`
values to override the ResultsRangeDict data.

:returns: An `IResultsRangeDict` compatible dict
:rtype: dict
"""
if self.dynamicspec is None:
return {}
# A matching Analysis Keyword is mandatory for any further matches
keyword = self.analysis.getKeyword()
by_keyword = self.dynamicspec.get_by_keyword()
# Get all specs (rows) from the Excel with the same Keyword
specs = by_keyword.get(keyword)
if not specs:
return {}

# Generate a match data object, which match both the column names and
# the field names of the Analysis.
match_data = self.get_match_data()

rr = {}

# Iterate over the rows and return the first where **all** values match
# with the analysis' values
for spec in specs:
skip = False

for k, v in match_data.items():
# break if the values do not match
if v != spec[k]:
skip = True
break

# skip the whole specification row
if skip:
continue

# at this point we have a match, update the results range dict
for key in self.range_keys:
value = spec.get(key, marker)
# skip if the range key is not set in the Excel
if value is marker:
continue
# skip if the value is not floatable
if not api.is_floatable(value):
continue
# set the range value
rr[key] = value
# return the updated result range
return rr

return rr

def __call__(self):
return self.get_results_range()
1 change: 1 addition & 0 deletions bika/lims/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<include file="calcs.zcml"/>
<include file="clientfolder.zcml"/>
<include file="contact.zcml"/>
<include file="dynamic_analysisspec.zcml"/>
<include file="instrument.zcml"/>
<include file="instrumentlocation.zcml"/>
<include file="instrumenttype.zcml"/>
Expand Down
72 changes: 72 additions & 0 deletions bika/lims/browser/dynamic_analysisspec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-

import collections

from bika.lims import _
from bika.lims import api
from senaite.core.listing.view import ListingView


class DynamicAnalysisSpecView(ListingView):
"""A listing view that shows the contents of the Excel
"""
def __init__(self, context, request):
super(DynamicAnalysisSpecView, self).__init__(context, request)

self.pagesize = 50
self.context_actions = {}
self.title = api.get_title(self.context)
self.description = api.get_description(self.context)
self.show_search = False
self.show_column_toggles = False

if self.context.specs_file:
filename = self.context.specs_file.filename
self.description = _("Contents of the file {}".format(filename))

self.specs = self.context.get_specs()
self.total = len(self.specs)

self.columns = collections.OrderedDict()
for title in self.context.get_header():
self.columns[title] = {
"title": title,
"toggle": True}

self.review_states = [
{
"id": "default",
"title": _("All"),
"contentFilter": {},
"transitions": [],
"custom_transitions": [],
"columns": self.columns.keys()
}
]

def update(self):
super(DynamicAnalysisSpecView, self).update()

def before_render(self):
super(DynamicAnalysisSpecView, self).before_render()

def make_empty_item(self, **kw):
"""Create a new empty item
"""
item = {
"uid": None,
"before": {},
"after": {},
"replace": {},
"allow_edit": [],
"disabled": False,
"state_class": "state-active",
}
item.update(**kw)
return item

def folderitems(self):
items = []
for record in self.specs:
items.append(self.make_empty_item(**record))
return items
17 changes: 17 additions & 0 deletions bika/lims/browser/dynamic_analysisspec.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="senaite.core">

<!-- Needed for cmf.AddPortalContent permission -->
<include package="Products.CMFCore" file="permissions.zcml" />

<browser:page
name="view"
for="bika.lims.content.dynamic_analysisspec.IDynamicAnalysisSpec"
class=".dynamic_analysisspec.DynamicAnalysisSpecView"
permission="zope2.View"
layer="bika.lims.interfaces.IBikaLIMS"
/>

</configure>
Binary file added bika/lims/browser/images/dynamic_analysisspec.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions bika/lims/browser/viewlets/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,18 @@
layer="bika.lims.interfaces.IBikaLIMS"
/>

<!-- Dynamic Specifications viewlet. Displays an informative message when the
specification has a dynamic specification assigned, so ranges might be
overriden by the ranges provided in the xls file from the Dynamic
Specification -->
<browser:viewlet
for="bika.lims.interfaces.IAnalysisSpec"
name="bika.lims.dynamic_spec_viewlet"
class=".dynamic_specs.DynamicSpecsViewlet"
manager="plone.app.layout.viewlets.interfaces.IAboveContent"
template="templates/dynamic_specs_viewlet.pt"
permission="zope2.View"
layer="bika.lims.interfaces.IBikaLIMS"
/>

</configure>
30 changes: 30 additions & 0 deletions bika/lims/browser/viewlets/dynamic_specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.CORE.
#
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2018-2020 by it's authors.
# Some rights reserved, see README and LICENSE.

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from plone.app.layout.viewlets import ViewletBase


class DynamicSpecsViewlet(ViewletBase):
""" Displays an informative message when the specification has a dynamic
specification assigned, so ranges might be overriden by the ranges provided
in the xls file from the Dynamic Specification
"""
template = ViewPageTemplateFile("templates/dynamic_specs_viewlet.pt")
25 changes: 25 additions & 0 deletions bika/lims/browser/viewlets/templates/dynamic_specs_viewlet.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div tal:omit-tag=""
tal:define="dynamic_spec python:context.getDynamicAnalysisSpec()"
tal:condition="python:dynamic_spec"
i18n:domain="senaite.core">

<div class="visualClear"></div>

<div id="portal-alert">
<div class="portlet-alert-item alert alert-warning alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong i18n:translate="">
This Analysis Specification has a Dynamic Specification assigned
</strong>
<p class="title">
<span i18n:translate="">
Be aware that the ranges provided in the spreadsheet file from the
dynamic specification might override the ranges defined in the
Specifications list below.
</span>
</p>
</div>
</div>
</div>
Loading