From 31b73517cc1f6ab84121864108eae90ee0223476 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 10:26:20 +0200 Subject: [PATCH 01/21] Add RemarksWidget --- bika/lims/browser/configure.zcml | 1 - bika/lims/browser/js/bika.lims.loader.js | 4 + bika/lims/browser/js/bika.lims.site.js | 2 +- .../browser/js/coffee/bika.lims.site.coffee | 2 +- .../coffee/bika_widgets/remarkswidget.coffee | 186 +++++++++++++ bika/lims/browser/remarks.py | 40 --- bika/lims/browser/remarks.zcml | 15 - bika/lims/browser/widgets/__init__.py | 1 + bika/lims/browser/widgets/remarkswidget.py | 18 ++ bika/lims/skins/bika/bika_widgets/remarks.js | 29 -- bika/lims/skins/bika/bika_widgets/remarks.pt | 118 -------- .../skins/bika/bika_widgets/remarkswidget.js | 258 ++++++++++++++++++ .../skins/bika/bika_widgets/remarkswidget.pt | 68 +++++ 13 files changed, 537 insertions(+), 205 deletions(-) create mode 100644 bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee delete mode 100644 bika/lims/browser/remarks.py delete mode 100644 bika/lims/browser/remarks.zcml create mode 100644 bika/lims/browser/widgets/remarkswidget.py delete mode 100644 bika/lims/skins/bika/bika_widgets/remarks.js delete mode 100644 bika/lims/skins/bika/bika_widgets/remarks.pt create mode 100644 bika/lims/skins/bika/bika_widgets/remarkswidget.js create mode 100644 bika/lims/skins/bika/bika_widgets/remarkswidget.pt diff --git a/bika/lims/browser/configure.zcml b/bika/lims/browser/configure.zcml index 38108a66d7..1069b1aa8e 100644 --- a/bika/lims/browser/configure.zcml +++ b/bika/lims/browser/configure.zcml @@ -27,7 +27,6 @@ - diff --git a/bika/lims/browser/js/bika.lims.loader.js b/bika/lims/browser/js/bika.lims.loader.js index 2406ea1014..8ab62c71fb 100644 --- a/bika/lims/browser/js/bika.lims.loader.js +++ b/bika/lims/browser/js/bika.lims.loader.js @@ -204,6 +204,10 @@ window.bika.lims.controllers = { ".portaltype-reportfolder": ['ReportFolderView'], + // If RemarksWidget is in use on this page, + // load RemarksWIdgetview + ".ArchetypesRemarksWidget": ["RemarksWidgetView"], + // Add here your view-controller/s assignment }; diff --git a/bika/lims/browser/js/bika.lims.site.js b/bika/lims/browser/js/bika.lims.site.js index 8676044510..7ee60ab45d 100644 --- a/bika/lims/browser/js/bika.lims.site.js +++ b/bika/lims/browser/js/bika.lims.site.js @@ -109,7 +109,7 @@ config: { closeOnEsc: false, onLoad: function() { - this.getOverlay().find('#archetypes-fieldname-Remarks').remove(); + this.getOverlay().find('.ArchetypesRemarksWidget').remove(); }, onClose: function() {} } diff --git a/bika/lims/browser/js/coffee/bika.lims.site.coffee b/bika/lims/browser/js/coffee/bika.lims.site.coffee index 016385faa2..8fcdfbfe27 100644 --- a/bika/lims/browser/js/coffee/bika.lims.site.coffee +++ b/bika/lims/browser/js/coffee/bika.lims.site.coffee @@ -118,7 +118,7 @@ class window.SiteView closeOnEsc: false onLoad: -> # manually remove remarks - @getOverlay().find('#archetypes-fieldname-Remarks').remove() + @getOverlay().find('.ArchetypesRemarksWidget').remove() return onClose: -> # here is where we'd populate the form controls, if we cared to. diff --git a/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee new file mode 100644 index 0000000000..07a1cc6c5d --- /dev/null +++ b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee @@ -0,0 +1,186 @@ +### Please use this command to compile this file into the proper folder: + coffee --no-header -w -o ../../../../skins/bika/bika_widgets -c remarkswidget.coffee +### + + +class window.RemarksWidgetView + + load: => + console.debug "RemarksWidgetView::load" + + # bind the event handler to the elements + @bind_eventhandler() + + ### INITIALIZERS ### + + bind_eventhandler: => + ### + * Binds callbacks on elements + * + * N.B. We attach all the events to the form and refine the selector to + * delegate the event: https://learn.jquery.com/events/event-delegation/ + ### + console.debug "RemarksWidgetView::bind_eventhandler" + + $("body").on "click", "input.saveRemarks", @on_remarks_submit + + # dev only + window.rem = @ + + ### METHODS ### + + get_remarks_widget: (uid) => + ### + * Shortcut to retrieve remarks widget from current page. + * if uid is not specified, attempts to find widget. + ### + if uid? + widgets = $(".ArchetypesRemarksWidget[data-uid='#{uid}']") + if widgets.length == 0 + console.warn "[RemarksWidgetView] No widget found with uid #{uid}" + return null + return $(widgets[0]) + else + widgets = $(".ArchetypesRemarksWidget") + if widgets.length == 0 + console.warn "[RemarksWidgetView] No widget found" + return null + if widgets.length > 1 + msg = "[RemarksWidgetView] Multiple widgets found, please specify uid" + console.warn msg + return null + return $(widgets[0]) + + format: (value) => + ### + * Output HTML string. + * + ### + remarks = value.replace(new RegExp("\n", "g"), "
") + return remarks + + update_remarks_history: (value, uid) => + ### + * Clear and update the widget's History with the provided value. + * + ### + widget = @get_remarks_widget(uid) + el = widget.find('.remarks_history') + el.html(@format value) + + clear_remarks_textarea: (uid) => + ### + * Clear textarea contents + * + ### + widget = @get_remarks_widget(uid) + el = widget.find('textarea') + el.val("") + + get_remarks: (uid) => + ### + * Return the value currently displayed for the widget's remarks + (HTML value) + * + ### + widget = @get_remarks_widget(uid) + return widget.find('.remarks_history').html() + + set_remarks: (value, uid) => + ### + * Single function to post remarks, update widget, and clear textarea. + * + ### + @post_remarks value, uid + .done (data) -> + @fetch_remarks uid + .done (remarks) -> + @update_remarks_history(remarks, uid) + @clear_remarks_textarea(uid) + + ### ASYNC DATA METHODS ### + + fetch_remarks: (uid) => + ### + * Get current value of field from /@@API/read + ### + + deferred = $.Deferred() + widget = @get_remarks_widget(uid) + + fieldname = widget.attr("data-fieldname") + + @ajax_submit + url: @get_portal_url() + "/@@API/read" + data: + catalog_name: "uid_catalog" + UID: widget.attr('data-uid') + include_fields: [fieldname] + .done (data) -> + return deferred.resolveWith this, [data.objects[0][fieldname]] + return deferred.promise() + + post_remarks: (value, uid) => + ### + * Submit the value to the field setter via /@@API/update. + * + ### + widget = @get_remarks_widget(uid) + fieldname = widget.attr("data-fieldname") + + deferred = $.Deferred() + options = + url: @get_portal_url() + "/@@API/update" + data: + obj_uid: widget.attr('data-uid') + options.data[fieldname] = value + @ajax_submit options + .done (data) -> + return deferred.resolveWith this, [[]] + return deferred.promise() + + ### EVENT HANDLERS ### + + on_remarks_submit: (event) => + ### + * Eventhandler for RemarksWidget"s "Save Remarks" button + * + ### + console.debug "°°° RemarksWidgetView::on_remarks_submit °°°" + event.preventDefault() + widget = $(event.currentTarget).parents(".ArchetypesRemarksWidget") + @set_remarks widget.children("textarea").val(), widget.attr('data-uid') + + ### HELPERS ### + + ajax_submit: (options) => + ###* + * Ajax Submit with automatic event triggering and some sane defaults + * + * @param {object} options + * jQuery ajax options + * @returns {Deferred} XHR request + ### + console.debug "°°° ajax_submit °°°" + + # some sane option defaults + options ?= {} + options.type ?= "POST" + options.url ?= @get_portal_url() + options.context ?= this + options.dataType ?= "json" + options.data ?= {} + + console.debug ">>> ajax_submit::options=", options + + $(this).trigger "ajax:submit:start" + done = -> + $(this).trigger "ajax:submit:end" + return $.ajax(options).done done + + get_portal_url: => + ### + * Return the portal url (calculated in code) + ### + url = $("input[name=portal_url]").val() + return url or window.portal_url diff --git a/bika/lims/browser/remarks.py b/bika/lims/browser/remarks.py deleted file mode 100644 index aef093fd4e..0000000000 --- a/bika/lims/browser/remarks.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of SENAITE.CORE -# -# Copyright 2018 by it's authors. -# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. - -from bika.lims.browser import BrowserView -from DateTime import DateTime -from AccessControl import getSecurityManager -from AccessControl import ClassSecurityInfo -from Products.CMFCore.utils import getToolByName -from Products.Archetypes.Widget import TextAreaWidget -from Products.Archetypes.Registry import registerWidget -import plone - -from plone.intelligenttext.transforms import \ - convertWebIntelligentPlainTextToHtml, \ - convertHtmlToWebIntelligentPlainText - -class ajaxSetRemarks(BrowserView): - """ Modify Remarks field and return new rendered field value - """ - def __call__(self): - plone.protect.CheckAuthenticator(self.request) - field = self.context.Schema()["Remarks"] - value = self.request['value'].strip() + "\n\n" - existing = self.context.getRemarks(mimetype='text/x-web-intelligent').strip() - - date = DateTime().rfc822() - user = getSecurityManager().getUser() - divider = "=== %s (%s)\n" % (date, user) - - remarks = convertWebIntelligentPlainTextToHtml(divider) + \ - convertWebIntelligentPlainTextToHtml(value) + \ - convertWebIntelligentPlainTextToHtml(existing) - - self.context.setRemarks(divider + value + existing, mimetype='text/x-web-intelligent') - - return remarks.strip() diff --git a/bika/lims/browser/remarks.zcml b/bika/lims/browser/remarks.zcml deleted file mode 100644 index db6f8a3f12..0000000000 --- a/bika/lims/browser/remarks.zcml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/bika/lims/browser/widgets/__init__.py b/bika/lims/browser/widgets/__init__.py index 4c7332f5d3..ab8b6d4726 100644 --- a/bika/lims/browser/widgets/__init__.py +++ b/bika/lims/browser/widgets/__init__.py @@ -8,6 +8,7 @@ from .datetimewidget import DateTimeWidget from .durationwidget import DurationWidget from .partitionsetupwidget import PartitionSetupWidget +from .remarkswidget import RemarksWidget from .recordswidget import RecordsWidget from .referenceresultswidget import ReferenceResultsWidget from .referencewidget import ReferenceWidget diff --git a/bika/lims/browser/widgets/remarkswidget.py b/bika/lims/browser/widgets/remarkswidget.py new file mode 100644 index 0000000000..412f5d0d6e --- /dev/null +++ b/bika/lims/browser/widgets/remarkswidget.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE +# +# Copyright 2018 by it's authors. +# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. + +from AccessControl import ClassSecurityInfo +from Products.Archetypes.Widget import TypesWidget + + +class RemarksWidget(TypesWidget): + _properties = TypesWidget._properties.copy() + _properties.update({ + 'macro': 'bika_widgets/remarkswidget', + }) + + security = ClassSecurityInfo() diff --git a/bika/lims/skins/bika/bika_widgets/remarks.js b/bika/lims/skins/bika/bika_widgets/remarks.js deleted file mode 100644 index 772f98851b..0000000000 --- a/bika/lims/skins/bika/bika_widgets/remarks.js +++ /dev/null @@ -1,29 +0,0 @@ -jQuery( function($) { -$(document).ready(function(){ - - // Hide remarks field if this object is in portal_factory - if(window.location.href.search("portal_factory") > -1) { - $("#archetypes-fieldname-Remarks").toggle(false); - } - - $('.saveRemarks').live('click', function(event){ - event.preventDefault(); - if ($("#Remarks").val() == '' || - $("#Remarks").val() == undefined) { - return false; - } - - $("#archetypes-fieldname-Remarks fieldset span").load( - $('#setRemarksURL').val() + "/setRemarksField", - {'value': $("#Remarks").val(), - '_authenticator': $('input[name="_authenticator"]').val()} - ); - $("#Remarks").val(""); - return false; - }); - $("#Remarks").empty(); - -}); -}); - - diff --git a/bika/lims/skins/bika/bika_widgets/remarks.pt b/bika/lims/skins/bika/bika_widgets/remarks.pt deleted file mode 100644 index 697f734ce5..0000000000 --- a/bika/lims/skins/bika/bika_widgets/remarks.pt +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - textarea - - - - - - - - -
- - characters remaining -
- -
- -
- -
- - History - - content -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.js b/bika/lims/skins/bika/bika_widgets/remarkswidget.js new file mode 100644 index 0000000000..8e55544bbc --- /dev/null +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.js @@ -0,0 +1,258 @@ + +/* Please use this command to compile this file into the proper folder: + coffee --no-header -w -o ../../../../skins/bika/bika_widgets -c remarkswidget.coffee + */ + +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + window.RemarksWidgetView = (function() { + function RemarksWidgetView() { + this.get_portal_url = bind(this.get_portal_url, this); + this.ajax_submit = bind(this.ajax_submit, this); + this.on_remarks_submit = bind(this.on_remarks_submit, this); + this.post_remarks = bind(this.post_remarks, this); + this.fetch_remarks = bind(this.fetch_remarks, this); + this.set_remarks = bind(this.set_remarks, this); + this.get_remarks = bind(this.get_remarks, this); + this.clear_remarks_textarea = bind(this.clear_remarks_textarea, this); + this.update_remarks_history = bind(this.update_remarks_history, this); + this.format = bind(this.format, this); + this.get_remarks_widget = bind(this.get_remarks_widget, this); + this.bind_eventhandler = bind(this.bind_eventhandler, this); + this.load = bind(this.load, this); + } + + RemarksWidgetView.prototype.load = function() { + console.debug("RemarksWidgetView::load"); + return this.bind_eventhandler(); + }; + + + /* INITIALIZERS */ + + RemarksWidgetView.prototype.bind_eventhandler = function() { + + /* + * Binds callbacks on elements + * + * N.B. We attach all the events to the form and refine the selector to + * delegate the event: https://learn.jquery.com/events/event-delegation/ + */ + console.debug("RemarksWidgetView::bind_eventhandler"); + $("body").on("click", "input.saveRemarks", this.on_remarks_submit); + return window.rem = this; + }; + + + /* METHODS */ + + RemarksWidgetView.prototype.get_remarks_widget = function(uid) { + + /* + * Shortcut to retrieve remarks widget from current page. + * if uid is not specified, attempts to find widget. + */ + var msg, widgets; + if (uid != null) { + widgets = $(".ArchetypesRemarksWidget[data-uid='" + uid + "']"); + if (widgets.length === 0) { + console.warn("[RemarksWidgetView] No widget found with uid " + uid); + return null; + } + return $(widgets[0]); + } else { + widgets = $(".ArchetypesRemarksWidget"); + if (widgets.length === 0) { + console.warn("[RemarksWidgetView] No widget found"); + return null; + } + if (widgets.length > 1) { + msg = "[RemarksWidgetView] Multiple widgets found, please specify uid"; + console.warn(msg); + return null; + } + } + return $(widgets[0]); + }; + + RemarksWidgetView.prototype.format = function(value) { + + /* + * Output HTML string. + * + */ + var remarks; + remarks = value.replace(new RegExp("\n", "g"), "
"); + return remarks; + }; + + RemarksWidgetView.prototype.update_remarks_history = function(value, uid) { + + /* + * Clear and update the widget's History with the provided value. + * + */ + var el, widget; + widget = this.get_remarks_widget(uid); + el = widget.find('.remarks_history'); + return el.html(this.format(value)); + }; + + RemarksWidgetView.prototype.clear_remarks_textarea = function(uid) { + + /* + * Clear textarea contents + * + */ + var el, widget; + widget = this.get_remarks_widget(uid); + el = widget.find('textarea'); + return el.val(""); + }; + + RemarksWidgetView.prototype.get_remarks = function(uid) { + + /* + * Return the value currently displayed for the widget's remarks + (HTML value) + * + */ + var widget; + widget = this.get_remarks_widget(uid); + return widget.find('.remarks_history').html(); + }; + + RemarksWidgetView.prototype.set_remarks = function(value, uid) { + + /* + * Single function to post remarks, update widget, and clear textarea. + * + */ + return this.post_remarks(value, uid).done(function(data) { + return this.fetch_remarks(uid).done(function(remarks) { + this.update_remarks_history(remarks, uid); + return this.clear_remarks_textarea(uid); + }); + }); + }; + + + /* ASYNC DATA METHODS */ + + RemarksWidgetView.prototype.fetch_remarks = function(uid) { + + /* + * Get current value of field from /@@API/read + */ + var deferred, fieldname, widget; + deferred = $.Deferred(); + widget = this.get_remarks_widget(uid); + fieldname = widget.attr("data-fieldname"); + this.ajax_submit({ + url: this.get_portal_url() + "/@@API/read", + data: { + catalog_name: "uid_catalog", + UID: widget.attr('data-uid'), + include_fields: [fieldname] + } + }).done(function(data) { + return deferred.resolveWith(this, [data.objects[0][fieldname]]); + }); + return deferred.promise(); + }; + + RemarksWidgetView.prototype.post_remarks = function(value, uid) { + + /* + * Submit the value to the field setter via /@@API/update. + * + */ + var deferred, fieldname, options, widget; + widget = this.get_remarks_widget(uid); + fieldname = widget.attr("data-fieldname"); + deferred = $.Deferred(); + options = { + url: this.get_portal_url() + "/@@API/update", + data: { + obj_uid: widget.attr('data-uid') + } + }; + options.data[fieldname] = value; + this.ajax_submit(options).done(function(data) { + return deferred.resolveWith(this, [[]]); + }); + return deferred.promise(); + }; + + + /* EVENT HANDLERS */ + + RemarksWidgetView.prototype.on_remarks_submit = function(event) { + + /* + * Eventhandler for RemarksWidget"s "Save Remarks" button + * + */ + var widget; + console.debug("°°° RemarksWidgetView::on_remarks_submit °°°"); + event.preventDefault(); + widget = $(event.currentTarget).parents(".ArchetypesRemarksWidget"); + return this.set_remarks(widget.children("textarea").val(), widget.attr('data-uid')); + }; + + + /* HELPERS */ + + RemarksWidgetView.prototype.ajax_submit = function(options) { + + /** + * Ajax Submit with automatic event triggering and some sane defaults + * + * @param {object} options + * jQuery ajax options + * @returns {Deferred} XHR request + */ + var done; + console.debug("°°° ajax_submit °°°"); + if (options == null) { + options = {}; + } + if (options.type == null) { + options.type = "POST"; + } + if (options.url == null) { + options.url = this.get_portal_url(); + } + if (options.context == null) { + options.context = this; + } + if (options.dataType == null) { + options.dataType = "json"; + } + if (options.data == null) { + options.data = {}; + } + console.debug(">>> ajax_submit::options=", options); + $(this).trigger("ajax:submit:start"); + done = function() { + return $(this).trigger("ajax:submit:end"); + }; + return $.ajax(options).done(done); + }; + + RemarksWidgetView.prototype.get_portal_url = function() { + + /* + * Return the portal url (calculated in code) + */ + var url; + url = $("input[name=portal_url]").val(); + return url || window.portal_url; + }; + + return RemarksWidgetView; + + })(); + +}).call(this); diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt new file mode 100644 index 0000000000..0c21381b5d --- /dev/null +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -0,0 +1,68 @@ + + + + + textarea + + + + + + + + Hide the widget's save button when rendering in portal_factory URLs: + XXX Need some abstracted way to instruct the widget to hide the save button + +
+ +
+
+ + +
+ History + + content + +
+
+ + + + + + + + + + + + + + + + + + + + From 71540c005bcb13a04f1e7d06b77f5787bacbf3d5 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 11:20:15 +0200 Subject: [PATCH 02/21] Add RemarksField --- bika/lims/browser/fields/remarksfield.py | 68 ++++++++++++++++++++++++ bika/lims/interfaces/__init__.py | 5 ++ 2 files changed, 73 insertions(+) create mode 100644 bika/lims/browser/fields/remarksfield.py diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py new file mode 100644 index 0000000000..978be6b957 --- /dev/null +++ b/bika/lims/browser/fields/remarksfield.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE +# +# Copyright 2018 by it's authors. +# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. + +from AccessControl import ClassSecurityInfo +from AccessControl import getSecurityManager +from DateTime import DateTime +from Products.Archetypes.Field import ObjectField +from Products.Archetypes.Registry import registerField +from bika.lims.browser.widgets import RemarksWidget +from bika.lims.interfaces import IRemarksField +from zope.interface import implements + + +class RemarksField(ObjectField): + """A field that stores remarks. The value submitted to the setter + will always be appended to the actual value of the field. + A divider will be included with extra information about the text. + """ + + _properties = ObjectField._properties.copy() + _properties.update({ + 'type': 'remarks', + 'widget': RemarksWidget, + 'default': '', + }) + + implements(IRemarksField) + + security = ClassSecurityInfo() + + security.declarePrivate('set') + + def set(self, instance, value, **kwargs): + """Adds the value to the existing text stored in the field, + along with a small divider showing username and date of this entry. + """ + if not value: + return + value = value.strip() + date = DateTime().rfc822() + user = getSecurityManager().getUser() + username = user.getUserName() + divider = "=== {} ({})".format(date, username) + existing_remarks = instance.getRawRemarks() + remarks = '\n'.join([divider, value, existing_remarks]) + return ObjectField.set(self, instance, remarks) + + def get(self, instance, **kwargs): + """Returns raw field value; the widget is responsible for converting + from newlines to breaks, or other clever formatting. + """ + value = ObjectField.get(self, instance, **kwargs) + # If field contains BaseUnit, then calling it returns the value. + if callable(value): + value = value() + return value + + def getRaw(self, instance, **kwargs): + """Returns raw field value (possible wrapped in BaseUnit) + """ + value = ObjectField.get(self, instance, **kwargs) + return value + +registerField(RemarksField, title='Remarks', description='') diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 335d3ec74f..d13374a187 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -823,6 +823,11 @@ class IProxyField(Interface): """ +class IRemarksField(Interface): + """An append-only TextField which saves information about each edit + """ + + class IARAnalysesField(Interface): """A field that manages AR Analyses """ From 22ffa7a19ecbaf0b111ede96758c3dd5a777ad3b Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 11:24:08 +0200 Subject: [PATCH 03/21] Fixup templates which manually render RemarksWidgets --- .../templates/analysisrequest_analyses.pt | 16 +++++++--------- .../analysisrequest_analyses_not_requested.pt | 17 +++++++++-------- .../templates/analysisrequest_manage_results.pt | 11 ++++++----- .../templates/analysisrequest_view.pt | 13 +++++++------ bika/lims/browser/sample/templates/sample.pt | 13 +++++++------ .../browser/templates/referencesample_view.pt | 14 ++++++++------ .../lims/browser/worksheet/templates/results.pt | 15 ++++++++------- 7 files changed, 52 insertions(+), 47 deletions(-) diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt index 568bfd2680..764250d927 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt @@ -115,15 +115,13 @@ - -

- -

- - - + + + -

- - - - + + + + diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt index 8e45fe83e8..d7d3f3aa8b 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_manage_results.pt @@ -46,11 +46,12 @@ - -

- + + - -

- + + - + diff --git a/bika/lims/browser/sample/templates/sample.pt b/bika/lims/browser/sample/templates/sample.pt index ecaaa7b886..cc6a332db9 100644 --- a/bika/lims/browser/sample/templates/sample.pt +++ b/bika/lims/browser/sample/templates/sample.pt @@ -44,12 +44,13 @@ - -

- - + + + diff --git a/bika/lims/browser/templates/referencesample_view.pt b/bika/lims/browser/templates/referencesample_view.pt index 7d778e07e6..fed7f373f5 100644 --- a/bika/lims/browser/templates/referencesample_view.pt +++ b/bika/lims/browser/templates/referencesample_view.pt @@ -138,12 +138,14 @@
- -

- - + + + + diff --git a/bika/lims/browser/worksheet/templates/results.pt b/bika/lims/browser/worksheet/templates/results.pt index 057238f542..2830b32036 100644 --- a/bika/lims/browser/worksheet/templates/results.pt +++ b/bika/lims/browser/worksheet/templates/results.pt @@ -138,14 +138,15 @@ - -

- - + + + - + From 54418f407f4f66e8782f329ecbac1b6fb673b0e9 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 11:25:38 +0200 Subject: [PATCH 04/21] Remove remarks from some templates where it should not be shown --- .../analysisrequest/templates/analysisrequests.pt | 8 -------- bika/lims/browser/templates/bika_listing.pt | 10 +--------- bika/lims/browser/worksheet/templates/add_analyses.pt | 6 ------ bika/lims/browser/worksheet/templates/add_control.pt | 7 ------- bika/lims/browser/worksheet/templates/add_duplicate.pt | 7 ------- 5 files changed, 1 insertion(+), 37 deletions(-) diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequests.pt b/bika/lims/browser/analysisrequest/templates/analysisrequests.pt index ba6307c7eb..60c8f64804 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequests.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequests.pt @@ -55,14 +55,6 @@

- - - - - - - diff --git a/bika/lims/browser/templates/bika_listing.pt b/bika/lims/browser/templates/bika_listing.pt index d5ef3cd5ae..3df0d7110c 100644 --- a/bika/lims/browser/templates/bika_listing.pt +++ b/bika/lims/browser/templates/bika_listing.pt @@ -52,15 +52,7 @@
- - -

- - - + diff --git a/bika/lims/browser/worksheet/templates/add_analyses.pt b/bika/lims/browser/worksheet/templates/add_analyses.pt index d3dde45058..01aef67bdc 100644 --- a/bika/lims/browser/worksheet/templates/add_analyses.pt +++ b/bika/lims/browser/worksheet/templates/add_analyses.pt @@ -127,12 +127,6 @@

- -

- - - diff --git a/bika/lims/browser/worksheet/templates/add_control.pt b/bika/lims/browser/worksheet/templates/add_control.pt index 308a087476..2967a9d613 100644 --- a/bika/lims/browser/worksheet/templates/add_control.pt +++ b/bika/lims/browser/worksheet/templates/add_control.pt @@ -57,13 +57,6 @@ - -

- - - diff --git a/bika/lims/browser/worksheet/templates/add_duplicate.pt b/bika/lims/browser/worksheet/templates/add_duplicate.pt index d7884cfb54..e472de69eb 100644 --- a/bika/lims/browser/worksheet/templates/add_duplicate.pt +++ b/bika/lims/browser/worksheet/templates/add_duplicate.pt @@ -40,13 +40,6 @@ - -

- - - From 1fb818c1470a25624e25875835999739a93aa4e1 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 11:33:49 +0200 Subject: [PATCH 05/21] Modify schemas to use RemarksWidget & RemarksField --- CHANGES.rst | 2 +- bika/lims/content/analysisprofile.py | 17 +- bika/lims/content/analysisrequest.py | 21 +- bika/lims/content/artemplate.py | 242 ++++++++++--------- bika/lims/content/batch.py | 12 +- bika/lims/content/instrumentcertification.py | 12 +- bika/lims/content/invoice.py | 11 +- bika/lims/content/pricelist.py | 59 ++--- bika/lims/content/referencesample.py | 16 +- bika/lims/content/sample.py | 16 +- bika/lims/content/supplier.py | 14 +- bika/lims/content/supplyorder.py | 12 +- bika/lims/content/worksheet.py | 149 ++++++------ 13 files changed, 293 insertions(+), 290 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8dcf4cb3ab..26ebd3ace1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,7 @@ Changelog - #905 Users created through LabContact's Login Details view are added to "Clients" group - #906 DateTime Widget does not display the Time - #909 List of clients cannot sort by Client ID -- #921 Missing interim fields in worksheet/analyses_transposed view +- #920 Refactored Remarks and created RemarksField and RemarksWidget **Security** diff --git a/bika/lims/content/analysisprofile.py b/bika/lims/content/analysisprofile.py index f9150ce135..e271541919 100644 --- a/bika/lims/content/analysisprofile.py +++ b/bika/lims/content/analysisprofile.py @@ -13,7 +13,9 @@ from AccessControl import ClassSecurityInfo from bika.lims import api from bika.lims import PMF, bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import AnalysisProfileAnalysesWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import ServicesWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema @@ -47,16 +49,11 @@ description = _("The analyses included in this profile, grouped per category"), ) ), - TextField('Remarks', - searchable = True, - default_content_type = 'text/plain', - allowable_content_types = ('text/plain', ), - default_output_type="text/plain", - widget = TextAreaWidget( - macro = "bika_widgets/remarks", - label = _("Remarks"), - append_only = True, - ), + RemarksField('Remarks', + searchable=True, + widget=RemarksWidget( + label=_("Remarks") + ) ), # Custom settings for the assigned analysis services # https://jira.bikalabs.com/browse/LIMS-1324 diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 7a146c371c..3e0f507fab 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -20,7 +20,9 @@ from bika.lims.browser.fields import ProxyField from bika.lims.browser.fields import UIDReferenceField # Bika Widgets +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import DateTimeWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import DecimalWidget from bika.lims.browser.widgets import PrioritySelectionWidget from bika.lims.browser.widgets import ReferenceWidget @@ -1487,25 +1489,18 @@ }, ), ), - TextField( + + RemarksField( 'Remarks', - default_content_type='text/x-web-intelligent', - allowable_content_types=('text/plain',), - default_output_type="text/plain", - mode="rw", - read_permission=View, - write_permission=ModifyPortalContent, - widget=TextAreaWidget( - macro="bika_widgets/remarks", + searchable=True, + widget=RemarksWidget( label=_("Remarks"), description=_("Remarks and comments for this request"), - append_only=True, + render_own_label=True, visible={ 'edit': 'visible', 'view': 'visible', - 'add': 'invisible', - 'sample_registered': - {'view': 'invisible', 'edit': 'invisible'}, + 'add': 'edit' }, ), ), diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index af05fd2e40..06055dd04c 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -11,41 +11,40 @@ """ from AccessControl import ClassSecurityInfo +from Products.ATExtensions.field.records import RecordsField from Products.Archetypes.public import * from Products.Archetypes.references import HoldingReference -from Products.CMFCore.permissions import View, ModifyPortalContent -from Products.ATExtensions.field.records import RecordsField from Products.CMFCore.utils import getToolByName from bika.lims import api -from bika.lims import PMF, bikaMessageFactory as _ -from bika.lims.interfaces import IARTemplate -from bika.lims.browser.widgets import RecordsWidget as BikaRecordsWidget -from bika.lims.browser.widgets import ARTemplatePartitionsWidget +from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import ARTemplateAnalysesWidget -from bika.lims.browser.widgets import RecordsWidget +from bika.lims.browser.widgets import ARTemplatePartitionsWidget from bika.lims.browser.widgets import ReferenceWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from zope.interface import Interface, implements -import sys +from bika.lims.interfaces import IARTemplate +from zope.interface import implements schema = BikaSchema.copy() + Schema(( - ## SamplePoint and SampleType references are managed with - ## accessors and mutators below to get/set a string value - ## (the Title of the object), but still store a normal Reference. - ## Form autocomplete widgets can then work with the Titles. - ReferenceField('SamplePoint', - vocabulary_display_path_bound = sys.maxint, - allowed_types = ('SamplePoint',), - relationship = 'ARTemplateSamplePoint', - referenceClass = HoldingReference, - accessor = 'getSamplePoint', - edit_accessor = 'getSamplePoint', - mutator = 'setSamplePoint', + # SamplePoint and SampleType references are managed with + # accessors and mutators below to get/set a string value + # (the Title of the object), but still store a normal Reference. + # Form autocomplete widgets can then work with the Titles. + ReferenceField( + 'SamplePoint', + allowed_types=('SamplePoint',), + relationship='ARTemplateSamplePoint', + referenceClass=HoldingReference, + accessor='getSamplePoint', + edit_accessor='getSamplePoint', + mutator='setSamplePoint', widget=ReferenceWidget( - label = _("Sample Point"), - description = _("Location where sample was taken"), - visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', + label=_("Sample Point"), + description=_("Location where sample was taken"), + visible={'edit': 'visible', 'view': 'visible', + 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, @@ -54,23 +53,25 @@ ), ComputedField( "SamplePointUID", - expression="context.Schema()['SamplePoint'].get(context) and context.Schema()['SamplePoint'].get(context).UID() or ''", + expression="context.Schema()['SamplePoint'].get(context) and " + "context.Schema()['SamplePoint'].get(context).UID() or ''", widget=ComputedWidget( visible=False, ), ), - ReferenceField('SampleType', - vocabulary_display_path_bound = sys.maxint, - allowed_types = ('SampleType',), - relationship = 'ARTemplateSampleType', - referenceClass = HoldingReference, - accessor = 'getSampleType', - edit_accessor = 'getSampleType', - mutator = 'setSampleType', + ReferenceField( + 'SampleType', + allowed_types=('SampleType',), + relationship='ARTemplateSampleType', + referenceClass=HoldingReference, + accessor='getSampleType', + edit_accessor='getSampleType', + mutator='setSampleType', widget=ReferenceWidget( - label = _("Sample Type"), - description = _("Create a new sample of this type"), - visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', + label=_("Sample Type"), + description=_("Create a new sample of this type"), + visible={'edit': 'visible', 'view': 'visible', + 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, @@ -79,105 +80,118 @@ ), ComputedField( "SampleTypeUID", - expression="context.Schema()['SampleType'].get(context) and context.Schema()['SampleType'].get(context).UID() or ''", + expression="context.Schema()['SampleType'].get(context) and " + "context.Schema()['SampleType'].get(context).UID() or ''", widget=ComputedWidget( visible=False, ), ), - BooleanField('Composite', + BooleanField( + 'Composite', default=False, widget=BooleanWidget( label=_("Composite"), description=_("It's a composite sample"), ), ), - TextField('Remarks', - searchable = True, - default_content_type = 'text/plain', - allowed_content_types= ('text/plain', ), - default_output_type="text/plain", - widget = TextAreaWidget( - macro = "bika_widgets/remarks", - label = _("Remarks"), - append_only = True, + RemarksField( + 'Remarks', + searchable=True, + widget=RemarksWidget( + label=_("Remarks"), ), ), - RecordsField('Partitions', - schemata = 'Sample Partitions', - required = 0, - type = 'artemplate_parts', - subfields = ('part_id', - 'Container', - 'Preservation', - 'container_uid', - 'preservation_uid'), - subfield_labels = {'part_id': _('Partition'), - 'Container': _('Container'), - 'Preservation': _('Preservation')}, - subfield_sizes = {'part_id': 15, - 'Container': 35, - 'Preservation': 35}, - subfield_hidden = {'preservation_uid': True, - 'container_uid': True}, - default = [{'part_id':'part-1', - 'Container':'', - 'Preservation':'', - 'container_uid':'', - 'preservation_uid':''}], + RecordsField( + 'Partitions', + schemata='Sample Partitions', + required=0, + type='artemplate_parts', + subfields=('part_id', + 'Container', + 'Preservation', + 'container_uid', + 'preservation_uid'), + subfield_labels={'part_id': _('Partition'), + 'Container': _('Container'), + 'Preservation': _('Preservation')}, + subfield_sizes={'part_id': 15, + 'Container': 35, + 'Preservation': 35}, + subfield_hidden={'preservation_uid': True, + 'container_uid': True}, + default=[{'part_id': 'part-1', + 'Container': '', + 'Preservation': '', + 'container_uid': '', + 'preservation_uid': ''}], widget=ARTemplatePartitionsWidget( - label = _("Sample Partitions"), - description = _("Configure the sample partitions and preservations " + \ - "for this template. Assign analyses to the different " + \ - "partitions on the template's Analyses tab"), + label=_("Sample Partitions"), + description=_( + "Configure the sample partitions and preservations " + + \ + "for this template. Assign analyses to the different " + "" + \ + "partitions on the template's Analyses tab"), combogrid_options={ 'Container': { 'colModel': [ - {'columnName':'container_uid', 'hidden':True}, - {'columnName':'Container', 'width':'30', 'label':_('Container')}, - {'columnName':'Description', 'width':'70', 'label':_('Description')}], + {'columnName': 'container_uid', + 'hidden': True}, + {'columnName': 'Container', 'width': '30', + 'label': _('Container')}, + {'columnName': 'Description', 'width': '70', + 'label': _('Description')}], 'url': 'getcontainers', 'showOn': True, 'width': '550px' }, 'Preservation': { 'colModel': [ - {'columnName':'preservation_uid', 'hidden':True}, - {'columnName':'Preservation', 'width':'30', 'label':_('Preservation')}, - {'columnName':'Description', 'width':'70', 'label':_('Description')}], + {'columnName': 'preservation_uid', + 'hidden': True}, + {'columnName': 'Preservation', 'width': '30', + 'label': _('Preservation')}, + {'columnName': 'Description', 'width': '70', + 'label': _('Description')}], 'url': 'getpreservations', 'showOn': True, 'width': '550px' }, }, - ), + ), ), - ReferenceField('AnalysisProfile', - schemata = 'Analyses', - required = 0, - multiValued = 0, - allowed_types = ('AnalysisProfile',), - relationship = 'ARTemplateAnalysisProfile', + ReferenceField( + 'AnalysisProfile', + schemata='Analyses', + required=0, + multiValued=0, + allowed_types=('AnalysisProfile',), + relationship='ARTemplateAnalysisProfile', widget=ReferenceWidget( - label = _("Analysis Profile"), - description =_("The Analysis Profile selection for this template"), - visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', + label=_("Analysis Profile"), + description=_( + "The Analysis Profile selection for this template"), + visible={'edit': 'visible', 'view': 'visible', + 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, showOn=True, ), ), - RecordsField('Analyses', - schemata = 'Analyses', - required = 0, - type = 'analyses', - subfields = ('service_uid', 'partition'), - subfield_labels = {'service_uid': _('Title'), - 'partition': _('Partition')}, - default = [], - widget = ARTemplateAnalysesWidget( - label = _("Analyses"), - description=_("Select analyses to include in this template"), + RecordsField( + 'Analyses', + schemata='Analyses', + required=0, + type='analyses', + subfields=('service_uid', 'partition'), + subfield_labels={'service_uid': _('Title'), + 'partition': _('Partition')}, + default=[], + widget=ARTemplateAnalysesWidget( + label=_("Analyses"), + description=_( + "Select analyses to include in this template"), ) ), # Custom settings for the assigned analysis services @@ -185,10 +199,11 @@ # Fields: # - uid: Analysis Service UID # - hidden: True/False. Hide/Display in results reports - RecordsField('AnalysisServicesSettings', - required=0, - subfields=('uid', 'hidden',), - widget=ComputedWidget(visible=False), + RecordsField( + 'AnalysisServicesSettings', + required=0, + subfields=('uid', 'hidden',), + widget=ComputedWidget(visible=False), ), ), ) @@ -199,6 +214,7 @@ # Update the validation layer after change the validator in runtime schema['title']._validationLayer() + class ARTemplate(BaseContent): security = ClassSecurityInfo() schema = schema @@ -206,22 +222,24 @@ class ARTemplate(BaseContent): implements(IARTemplate) _at_rename_after_creation = True + def _renameAfterCreation(self, check_auto_id=False): from bika.lims.idserver import renameAfterCreation renameAfterCreation(self) security.declarePublic('AnalysisProfiles') + def AnalysisProfiles(self, instance=None): instance = instance or self bsc = getToolByName(instance, 'bika_setup_catalog') items = [] for p in bsc(portal_type='AnalysisProfile', - inactive_state='active', - sort_on = 'sortable_title'): + inactive_state='active', + sort_on='sortable_title'): p = p.getObject() title = p.Title() items.append((p.UID(), title)) - items = [['','']] + list(items) + items = [('', '')] + list(items) return DisplayList(items) def getClientUID(self): @@ -230,7 +248,6 @@ def getClientUID(self): then that folder's UID must be returned in this index. """ return self.aq_parent.UID() - return '' def getAnalysisServiceSettings(self, uid): """ Returns a dictionary with the settings for the analysis @@ -238,8 +255,8 @@ def getAnalysisServiceSettings(self, uid): If there are no settings for the analysis service and template, returns a dictionary with the key 'uid' """ - sets = [s for s in self.getAnalysisServicesSettings() \ - if s.get('uid','') == uid] + sets = [s for s in self.getAnalysisServicesSettings() + if s.get('uid', '') == uid] return sets[0] if sets else {'uid': uid} def isAnalysisServiceHidden(self, uid): @@ -264,7 +281,6 @@ def isAnalysisServiceHidden(self, uid): raise ValueError('%s is not valid' % uid) return sets.get('hidden', False) - def remove_service(self, service): """Removes the service passed in from the services offered by the current Template. If the Analysis Service passed in is not assigned to @@ -278,7 +294,8 @@ def remove_service(self, service): # Remove the service from the referenced services services = self.getAnalyses() num_services = len(services) - services = [item for item in services if item.get('service_uid', '') != uid] + services = [item for item in services if + item.get('service_uid', '') != uid] removed = len(services) < num_services self.setAnalyses(services) @@ -289,4 +306,5 @@ def remove_service(self, service): return removed + registerType(ARTemplate, PROJECTNAME) diff --git a/bika/lims/content/batch.py b/bika/lims/content/batch.py index 2c85a8105e..48f7eb171f 100644 --- a/bika/lims/content/batch.py +++ b/bika/lims/content/batch.py @@ -8,7 +8,9 @@ from AccessControl import ClassSecurityInfo from bika.lims import bikaMessageFactory as _ from bika.lims import deprecated +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import RecordsWidget as bikaRecordsWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import DateTimeWidget, ReferenceWidget from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING from bika.lims.config import PROJECTNAME @@ -132,15 +134,11 @@ def set(self, instance, value, **kwargs): ) ), - TextField( + RemarksField( 'Remarks', - default_content_type='text/x-web-intelligent', - allowable_content_types=('text/plain', ), - default_output_type="text/plain", - widget=TextAreaWidget( - macro="bika_widgets/remarks", + searchable=True, + widget=RemarksWidget( label=_('Remarks'), - append_only=True, ) ), diff --git a/bika/lims/content/instrumentcertification.py b/bika/lims/content/instrumentcertification.py index 03abbfcdef..203cb776c6 100644 --- a/bika/lims/content/instrumentcertification.py +++ b/bika/lims/content/instrumentcertification.py @@ -29,7 +29,9 @@ from Products.CMFCore.utils import getToolByName from bika.lims import bikaMessageFactory as _ # bika.lims imports +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import ComboBoxWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import DateTimeWidget from bika.lims.browser.widgets import ReferenceWidget from bika.lims import logger @@ -179,17 +181,11 @@ ) ), - TextField( + RemarksField( 'Remarks', searchable=True, - default_content_type='text/x-web-intelligent', - allowable_content_types=('text/plain', ), - default_output_type="text/plain", - mode="rw", - widget=TextAreaWidget( - macro="bika_widgets/remarks", + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), diff --git a/bika/lims/content/invoice.py b/bika/lims/content/invoice.py index 56a0fed167..10a8ade2e1 100644 --- a/bika/lims/content/invoice.py +++ b/bika/lims/content/invoice.py @@ -7,6 +7,8 @@ from AccessControl import ClassSecurityInfo from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import RemarksWidget from bika.lims.utils import t from bika.lims.config import ManageInvoices, ManageBika, PROJECTNAME from bika.lims.content.bikaschema import BikaSchema @@ -51,16 +53,11 @@ label=_("Date"), ), ), - TextField( + RemarksField( 'Remarks', searchable=True, - default_content_type='text/plain', - allowed_content_types=('text/plain', ), - default_output_type="text/plain", - widget=TextAreaWidget( - macro="bika_widgets/remarks", + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), ComputedField( diff --git a/bika/lims/content/pricelist.py b/bika/lims/content/pricelist.py index 1512613695..91cfaaad57 100644 --- a/bika/lims/content/pricelist.py +++ b/bika/lims/content/pricelist.py @@ -6,25 +6,22 @@ # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. from AccessControl import ClassSecurityInfo +from DateTime import DateTime +from Products.Archetypes.public import * +from Products.CMFCore import permissions from bika.lims import bikaMessageFactory as _ -from bika.lims.utils import t -from bika.lims.browser.widgets.datetimewidget import DateTimeWidget +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PRICELIST_TYPES, PROJECTNAME from bika.lims.content.bikaschema import BikaFolderSchema from bika.lims.interfaces import IPricelist -from DateTime import DateTime from persistent.mapping import PersistentMapping from plone.app.folder import folder -from Products.Archetypes.public import * -from Products.CMFCore import permissions from zope.interface import implements -from Products.CMFCore.utils import getToolByName -from Products.CMFCore import permissions - - schema = BikaFolderSchema.copy() + Schema(( - StringField('Type', + StringField( + 'Type', required=1, vocabulary=PRICELIST_TYPES, widget=SelectionWidget( @@ -32,34 +29,33 @@ label=_("Pricelist for"), ), ), - BooleanField('BulkDiscount', + BooleanField( + 'BulkDiscount', default=False, widget=SelectionWidget( label=_("Bulk discount applies"), ), ), - FixedPointField('BulkPrice', + FixedPointField( + 'BulkPrice', widget=DecimalWidget( label=_("Discount %"), description=_("Enter discount percentage value"), ), ), - BooleanField('Descriptions', + BooleanField( + 'Descriptions', default=False, widget=BooleanWidget( label=_("Include descriptions"), description=_("Select if the descriptions should be included"), ), ), - TextField('Remarks', + RemarksField( + 'Remarks', searchable=True, - default_content_type='text/plain', - allowed_content_types=('text/plain', ), - default_output_type="text/plain", - widget=TextAreaWidget( - macro="bika_widgets/remarks", + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), ), @@ -71,13 +67,13 @@ Field = schema['effectiveDate'] Field.schemata = 'default' -Field.required = 0 # "If no date is selected the item will be published - #immediately." +Field.required = 0 # "If no date is selected the item will be published +# immediately." Field.widget.visible = True Field = schema['expirationDate'] Field.schemata = 'default' -Field.required = 0 # "If no date is chosen, it will never expire." +Field.required = 0 # "If no date is chosen, it will never expire." Field.widget.visible = True @@ -114,9 +110,11 @@ def current_date(self): security.declareProtected(permissions.ModifyPortalContent, 'processForm') + registerType(Pricelist, PROJECTNAME) +# noinspection PyUnusedLocal def ObjectModifiedEventHandler(instance, event): """ Various types need automation on edit. """ @@ -128,11 +126,16 @@ def ObjectModifiedEventHandler(instance, event): """ # Remove existing line items instance.pricelist_lineitems = [] - for p in instance.portal_catalog(portal_type=instance.getType(), - inactive_state="active"): + for p in instance.portal_catalog( + portal_type=instance.getType(), inactive_state="active"): obj = p.getObject() itemDescription = None itemAccredited = False + price = '' + vat = '' + totalprice = '' + cat = '' + itemTitle = '' if instance.getType() == "LabProduct": print_detail = "" if obj.getVolume(): @@ -164,9 +167,9 @@ def ObjectModifiedEventHandler(instance, event): # cat = obj.getCategoryTitle() if instance.getBulkDiscount(): - price = float(obj.getBulkPrice()) - vat = get_vat_amount(price, obj.getVAT()) - totalprice = price + vat + price = float(obj.getBulkPrice()) + vat = get_vat_amount(price, obj.getVAT()) + totalprice = price + vat else: if instance.getBulkPrice(): discount = instance.getBulkPrice() diff --git a/bika/lims/content/referencesample.py b/bika/lims/content/referencesample.py index b22666c5ac..a10094d73e 100644 --- a/bika/lims/content/referencesample.py +++ b/bika/lims/content/referencesample.py @@ -19,10 +19,12 @@ from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import _createObjectByType from bika.lims import PMF, bikaMessageFactory as _, api +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.idserver import renameAfterCreation from bika.lims.utils import t from bika.lims.browser.fields import ReferenceResultsField from bika.lims.browser.widgets import DateTimeWidget as bika_DateTimeWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import ReferenceResultsWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema @@ -84,16 +86,12 @@ label=_("Lot Number"), ), ), - TextField('Remarks', - schemata = 'Description', - searchable = True, - default_content_type = 'text/plain', - allowed_content_types= ('text/plain', ), - default_output_type="text/plain", - widget = TextAreaWidget( - macro = "bika_widgets/remarks", + RemarksField( + 'Remarks', + schemata='Description', + searchable=True, + widget=RemarksWidget( label=_("Remarks"), - append_only = True, ), ), DateTimeField('DateSampled', diff --git a/bika/lims/content/sample.py b/bika/lims/content/sample.py index 4a790346cf..1c9556cfdd 100644 --- a/bika/lims/content/sample.py +++ b/bika/lims/content/sample.py @@ -11,12 +11,14 @@ from Products.CMFCore.WorkflowCore import WorkflowException from bika.lims import bikaMessageFactory as _, logger from bika.lims.api import get_object_by_uid +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.fields.uidreferencefield import get_backreferences from bika.lims.utils import t, getUsers from Products.ATExtensions.field import RecordsField from bika.lims import deprecated from bika.lims.browser.widgets.datetimewidget import DateTimeWidget from bika.lims.browser.widgets import RejectionWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.interfaces import ISample, ISamplePrepWorkflow @@ -606,17 +608,11 @@ render_own_label=True, ), ), - TextField('Remarks', - default_content_type='text/x-web-intelligent', - allowable_content_types = ('text/plain', ), - default_output_type="text/plain", - mode="rw", - read_permission=permissions.View, - write_permission=permissions.ModifyPortalContent, - widget=TextAreaWidget( - macro="bika_widgets/remarks", + RemarksField( + 'Remarks', + searchable=True, + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), RecordsField('RejectionReasons', diff --git a/bika/lims/content/supplier.py b/bika/lims/content/supplier.py index a4303b0010..948b8c78ca 100644 --- a/bika/lims/content/supplier.py +++ b/bika/lims/content/supplier.py @@ -7,6 +7,8 @@ from AccessControl import ClassSecurityInfo from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import RemarksWidget from bika.lims.utils import t from bika.lims.config import PROJECTNAME, ManageSuppliers from bika.lims.content.bikaschema import BikaSchema @@ -18,15 +20,11 @@ from zope.interface import implements schema = Organisation.schema.copy() + ManagedSchema(( - TextField('Remarks', - searchable = True, - default_content_type = 'text/plain', - allowed_content_types= ('text/plain', ), - default_output_type = "text/html", - widget = TextAreaWidget( - macro = "bika_widgets/remarks", + RemarksField( + 'Remarks', + searchable=True, + widget=RemarksWidget( label=_("Remarks"), - append_only = True, ), ), StringField('Website', diff --git a/bika/lims/content/supplyorder.py b/bika/lims/content/supplyorder.py index 7d88b7076c..2d69643de4 100644 --- a/bika/lims/content/supplyorder.py +++ b/bika/lims/content/supplyorder.py @@ -11,7 +11,9 @@ from AccessControl import ClassSecurityInfo from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import DateTimeWidget +from bika.lims.browser.widgets import RemarksWidget from bika.lims.browser.widgets import ReferenceWidget as BikaReferenceWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema @@ -81,15 +83,11 @@ label=_("Date Dispatched"), ), ), - TextField('Remarks', + RemarksField( + 'Remarks', searchable=True, - default_content_type='text/plain', - allowed_content_types=('text/plain', ), - default_output_type="text/plain", - widget = TextAreaWidget( - macro="bika_widgets/remarks", + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), ComputedField('ClientUID', diff --git a/bika/lims/content/worksheet.py b/bika/lims/content/worksheet.py index 060da40357..f32497c80b 100644 --- a/bika/lims/content/worksheet.py +++ b/bika/lims/content/worksheet.py @@ -9,37 +9,43 @@ import sys from AccessControl import ClassSecurityInfo -from bika.lims import bikaMessageFactory as _ +from DateTime import DateTime +from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin +from Products.ATExtensions.ateapi import RecordsField +from Products.Archetypes.config import REFERENCE_CATALOG +from Products.Archetypes.public import BaseFolder +from Products.Archetypes.public import DisplayList +from Products.Archetypes.public import ReferenceField +from Products.Archetypes.public import Schema +from Products.Archetypes.public import SelectionWidget +from Products.Archetypes.public import StringField +from Products.Archetypes.public import registerType +from Products.Archetypes.references import HoldingReference +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.utils import _createObjectByType, safe_unicode from bika.lims import api, deprecated, logger +from bika.lims import bikaMessageFactory as _ from bika.lims.browser.fields import UIDReferenceField +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME, WORKSHEET_LAYOUT_OPTIONS from bika.lims.content.bikaschema import BikaSchema from bika.lims.idserver import renameAfterCreation -from bika.lims.interfaces import (IAnalysisRequest, IDuplicateAnalysis, - IReferenceAnalysis, IReferenceSample, - IRoutineAnalysis, IWorksheet) -from bika.lims.permissions import Verify as VerifyPermission +from bika.lims.interfaces import IAnalysisRequest +from bika.lims.interfaces import IDuplicateAnalysis +from bika.lims.interfaces import IReferenceAnalysis +from bika.lims.interfaces import IReferenceSample +from bika.lims.interfaces import IRoutineAnalysis +from bika.lims.interfaces import IWorksheet from bika.lims.permissions import EditWorksheet, ManageWorksheets -from bika.lims.utils import to_utf8 as _c +from bika.lims.permissions import Verify as VerifyPermission from bika.lims.utils import changeWorkflowState, tmpID, to_int +from bika.lims.utils import to_utf8 as _c from bika.lims.workflow import doActionFor, getCurrentState, skip from bika.lims.workflow.worksheet import events, guards -from DateTime import DateTime from plone.api.user import has_permission -from Products.Archetypes.config import REFERENCE_CATALOG -from Products.Archetypes.public import (BaseFolder, DisplayList, - ReferenceField, Schema, - SelectionWidget, StringField, - TextAreaWidget, TextField, - registerType) -from Products.Archetypes.references import HoldingReference -from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin -from Products.ATExtensions.ateapi import RecordsField -from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import _createObjectByType, safe_unicode from zope.interface import implements - ALL_ANALYSES_TYPES = "all" ALLOWED_ANALYSES_TYPES = ["a", "b", "c", "d"] @@ -63,7 +69,8 @@ 'Analyses', required=1, multiValued=1, - allowed_types=('Analysis', 'DuplicateAnalysis', 'ReferenceAnalysis', 'RejectAnalysis'), + allowed_types=('Analysis', 'DuplicateAnalysis', 'ReferenceAnalysis', + 'RejectAnalysis'), relationship='WorksheetAnalysis', ), @@ -97,16 +104,11 @@ referenceClass=HoldingReference, ), - TextField( + RemarksField( 'Remarks', searchable=True, - default_content_type='text/plain', - allowed_content_types=('text/plain', ), - default_output_type="text/plain", - widget=TextAreaWidget( - macro="bika_widgets/remarks", + widget=RemarksWidget( label=_("Remarks"), - append_only=True, ), ), @@ -185,13 +187,15 @@ def addAnalysis(self, analysis, position=None): self.setAnalyses(analyses + [analysis, ]) # if our parent has a position, use that one. - if analysis.aq_parent.UID() in [slot['container_uid'] for slot in layout]: + uids = [slot['container_uid'] for slot in layout] + if analysis.aq_parent.UID() in uids: position = [int(slot['position']) for slot in layout if slot['container_uid'] == analysis.aq_parent.UID()][0] else: # prefer supplied position parameter if not position: - used_positions = [0, ] + [int(slot['position']) for slot in layout] + positions = [int(slot['position']) for slot in layout] + used_positions = [0, ] + positions position = [pos for pos in range(1, max(used_positions) + 2) if pos not in used_positions][0] self.setLayout(layout + [{'position': position, @@ -312,7 +316,7 @@ def addReferenceAnalyses(self, reference, service_uids, dest_slot=None): refgid) if ref_analysis: - # All ref analyses from the same slot must have the same group id + # All ref analyses from the same slot must have same group id refgid = ref_analysis.getReferenceAnalysesGroupID() ref_analyses.append(ref_analysis) return ref_analyses @@ -548,7 +552,7 @@ def _get_suitable_slot_for_duplicate(self, src_slot): # those that belong to an Analysis Request. return 0 - occupied = self.get_slot_positions(type='all') + occupied = self.get_slot_positions(typ='all') wst = self.getWorksheetTemplate() if not wst: # No worksheet template assigned, add a new slot at the end of @@ -594,7 +598,7 @@ def _get_suitable_slot_for_references(self, reference): if not IReferenceSample.providedBy(reference): return 0 - occupied = self.get_slot_positions(type='all') + occupied = self.get_slot_positions(typ='all') wst = self.getWorksheetTemplate() if not wst: # No worksheet template assigned, add a new slot at the end of the @@ -700,7 +704,7 @@ def get_container_at(self, slot): return None - def get_slot_positions(self, type='a'): + def get_slot_positions(self, typ=None): """Returns a list with the slots occupied for the type passed in. Allowed type of analyses are: @@ -711,44 +715,47 @@ def get_slot_positions(self, type='a'): 'd' (duplicate) 'all' (all analyses) - :param type: type of the analysis + :param typ: type of the analysis :return: list of slot positions """ - if type not in ALLOWED_ANALYSES_TYPES and type != ALL_ANALYSES_TYPES: + typ = typ if typ else 'a' + if typ not in ALLOWED_ANALYSES_TYPES and typ != ALL_ANALYSES_TYPES: return list() layout = self.getLayout() slots = list() for pos in layout: - if type != ALL_ANALYSES_TYPES and pos['type'] != type: + if typ != ALL_ANALYSES_TYPES and pos['type'] != typ: continue slots.append(to_int(pos['position'])) # return a unique list of sorted slot positions return sorted(set(slots)) - def get_slot_position(self, container, type='a'): - """Returns the slot where the analyses from the type and container passed + def get_slot_position(self, container, typ=None): + """Returns the slot where the analyses from the type and container + passed in are located within the worksheet. :param container: the container in which the analyses are grouped - :param type: type of the analysis + :param typ: type of the analysis :return: the slot position :rtype: int """ - if not container or type not in ALLOWED_ANALYSES_TYPES: + typ = typ if typ else 'a' + if not container or typ not in ALLOWED_ANALYSES_TYPES: return None uid = api.get_uid(container) layout = self.getLayout() for pos in layout: - if pos['type'] != type or pos['container_uid'] != uid: + if pos['type'] != typ or pos['container_uid'] != uid: continue return to_int(pos['position']) return None - def resolve_available_slots(self, worksheet_template, type='a'): + def resolve_available_slots(self, worksheet_template, typ=None): """Returns the available slots from the current worksheet that fits with the layout defined in the worksheet_template and type of analysis passed in. @@ -761,19 +768,20 @@ def resolve_available_slots(self, worksheet_template, type='a'): 'd' (duplicate) :param worksheet_template: the worksheet template to match against - :param type: type of analyses to restrict that suit with the slots + :param typ: type of analyses to restrict that suit with the slots :return: a list of slots positions """ - if not worksheet_template or type not in ALLOWED_ANALYSES_TYPES: + typ = typ if typ else 'a' + if not worksheet_template or typ not in ALLOWED_ANALYSES_TYPES: return list() - ws_slots = self.get_slot_positions(type) + ws_slots = self.get_slot_positions(typ) layout = worksheet_template.getLayout() slots = list() for row in layout: # skip rows that do not match with the given type - if row['type'] != type: + if row['type'] != typ: continue slot = to_int(row['pos']) @@ -980,23 +988,23 @@ def _resolve_reference_sample(self, reference_samples=None, return best_sample, best_supported - def _resolve_reference_samples(self, wst, type): + def _resolve_reference_samples(self, wst, typ): """ Resolves the slots and reference samples in accordance with the Worksheet Template passed in and the type passed in. Returns a list of dictionaries :param wst: Worksheet Template that defines the layout - :param type: type of analyses ('b' for blanks, 'c' for controls) + :param typ: type of analyses ('b' for blanks, 'c' for controls) :return: list of dictionaries """ - if not type or type not in ['b', 'c']: + if not typ or typ not in ['b', 'c']: return [] bc = api.get_tool("bika_catalog") - wst_type = type == 'b' and 'blank_ref' or 'control_ref' + wst_type = typ == 'b' and 'blank_ref' or 'control_ref' slots_sample = list() - available_slots = self.resolve_available_slots(wst, type) + available_slots = self.resolve_available_slots(wst, typ) wst_layout = wst.getLayout() for row in wst_layout: slot = int(row['pos']) @@ -1021,8 +1029,8 @@ def _resolve_reference_samples(self, wst, type): candidates = list() for sample in samples: obj = api.get_object(sample) - if (type == 'b' and obj.getBlank()) or \ - (type == 'c' and not obj.getBlank()): + if (typ == 'b' and obj.getBlank()) or \ + (typ == 'c' and not obj.getBlank()): candidates.append(obj) sample, uids = self._resolve_reference_sample(candidates, services) @@ -1035,21 +1043,22 @@ def _resolve_reference_samples(self, wst, type): return slots_sample - def _apply_worksheet_template_reference_analyses(self, wst, type='all'): + def _apply_worksheet_template_reference_analyses(self, wst, typ=None): """ Add reference analyses to worksheet according to the worksheet template layout passed in. Does not overwrite slots that are already filled. :param wst: worksheet template used as the layout """ - if type == 'all': + typ = typ if typ else 'all' + if typ == 'all': self._apply_worksheet_template_reference_analyses(wst, 'b') self._apply_worksheet_template_reference_analyses(wst, 'c') return - if type not in ['b', 'c']: + if typ not in ['b', 'c']: return - references = self._resolve_reference_samples(wst, type) + references = self._resolve_reference_samples(wst, typ) for reference in references: slot = reference['slot'] sample = reference['sample'] @@ -1234,6 +1243,7 @@ def getNumberOfRegularSamples(self): security.declareProtected(EditWorksheet, 'resequenceWorksheet') + # noinspection PyUnusedLocal def resequenceWorksheet(self, REQUEST=None, RESPONSE=None): """ Reset the sequence of analyses in the worksheet """ """ sequence is [{'pos': , 'type': , 'uid', 'key'},] """ @@ -1345,6 +1355,7 @@ def setMethod(self, method, override_analyses=False): self.getField('Method').set(self, method) return total + # noinspection PyUnusedLocal @deprecated('[1703] Orphan. No alternative') def getFolderContents(self, contentFilter): """ @@ -1496,10 +1507,10 @@ def copy_src_fields_to_dst(src, dst): fieldname = field.getName() if fieldname in ignore_fields: continue - getter = getattr(src, 'get' + fieldname, - src.Schema().getField(fieldname).getAccessor(src)) - setter = getattr(dst, 'set' + fieldname, - dst.Schema().getField(fieldname).getMutator(dst)) + accessor = src.Schema().getField(fieldname).getAccessor(src) + mutator = dst.Schema().getField(fieldname).getMutator(dst) + getter = getattr(src, 'get' + fieldname, accessor) + setter = getattr(dst, 'set' + fieldname, mutator) if getter is None or setter is None: # ComputedField continue @@ -1535,12 +1546,11 @@ def copy_src_fields_to_dst(src, dst): new_ws_analyses = [] old_ws_analyses = [] for analysis in analyses: + position = analysis_positions[analysis.UID()] # Skip published or verified analyses review_state = workflow.getInfoFor(analysis, 'review_state', '') if review_state in ['published', 'verified', 'retracted']: old_ws_analyses.append(analysis.UID()) - - # XXX where does position come from? old_layout.append({'position': position, 'type': 'a', 'analysis_uid': analysis.UID(), @@ -1562,7 +1572,6 @@ def copy_src_fields_to_dst(src, dst): Retested=True, ) analysis.reindexObject() - position = analysis_positions[analysis.UID()] old_ws_analyses.append(reject.UID()) old_layout.append({'position': position, 'type': 'r', @@ -1580,9 +1589,8 @@ def copy_src_fields_to_dst(src, dst): service_uid = analysis.getServiceUID() reference = analysis.aq_parent reference_type = analysis.getReferenceType() - new_analysis_uid = reference.addReferenceAnalysis(service_uid, - reference_type) - position = analysis_positions[analysis.UID()] + new_analysis_uid = reference.addReferenceAnalysis( + service_uid, reference_type) old_ws_analyses.append(analysis.UID()) old_layout.append({'position': position, 'type': reference_type, @@ -1594,7 +1602,8 @@ def copy_src_fields_to_dst(src, dst): 'analysis_uid': new_analysis_uid, 'container_uid': reference.UID()}) workflow.doActionFor(analysis, 'reject') - new_reference = reference.uid_catalog(UID=new_analysis_uid)[0].getObject() + brains = reference.uid_catalog(UID=new_analysis_uid) + new_reference = brains[0].getObject() workflow.doActionFor(new_reference, 'assign') analysis.reindexObject() # Duplicate analyses @@ -1608,7 +1617,6 @@ def copy_src_fields_to_dst(src, dst): copy_src_fields_to_dst(analysis, new_duplicate) workflow.doActionFor(new_duplicate, 'assign') new_duplicate.reindexObject() - position = analysis_positions[analysis.UID()] old_ws_analyses.append(analysis.UID()) old_layout.append({'position': position, 'type': 'd', @@ -1628,7 +1636,8 @@ def copy_src_fields_to_dst(src, dst): for analysis in new_ws.getAnalyses(): review_state = workflow.getInfoFor(analysis, 'review_state', '') if review_state == 'to_be_verified': - changeWorkflowState(analysis, "bika_analysis_workflow", "sample_received") + changeWorkflowState( + analysis, "bika_analysis_workflow", "sample_received") self.REQUEST['context_uid'] = self.UID() self.setLayout(old_layout) self.setAnalyses(old_ws_analyses) From 4339613c8bd81fd72583dd30afd9094033066afc Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 11:37:55 +0200 Subject: [PATCH 06/21] Update changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac18a6bbaf..369ee1050c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,7 @@ Changelog - #905 Users created through LabContact's Login Details view are added to "Clients" group - #906 DateTime Widget does not display the Time - #909 List of clients cannot sort by Client ID +- #921 Missing interim fields in worksheet/analyses_transposed view - #920 Refactored Remarks and created RemarksField and RemarksWidget **Security** From 79623633f4c1d91e185113b74c6a29696d0c1ab5 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 26 Jul 2018 14:22:47 +0200 Subject: [PATCH 07/21] Always display history box to allow setting remarks asynchronously --- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index 0c21381b5d..2d0d34d61c 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -38,9 +38,8 @@ -

- History +
+ History content From 4dbe325cb263d8402d45002d13d25656b4ca6e45 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 26 Jul 2018 14:24:01 +0200 Subject: [PATCH 08/21] Some error handling in the JS logic --- .../coffee/bika_widgets/remarkswidget.coffee | 21 ++++++++++++---- .../skins/bika/bika_widgets/remarkswidget.js | 24 +++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee index 07a1cc6c5d..9e208916d1 100644 --- a/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee +++ b/bika/lims/browser/js/coffee/bika_widgets/remarkswidget.coffee @@ -54,7 +54,6 @@ class window.RemarksWidgetView format: (value) => ### * Output HTML string. - * ### remarks = value.replace(new RegExp("\n", "g"), "
") return remarks @@ -62,18 +61,18 @@ class window.RemarksWidgetView update_remarks_history: (value, uid) => ### * Clear and update the widget's History with the provided value. - * ### widget = @get_remarks_widget(uid) + return if widget is null el = widget.find('.remarks_history') el.html(@format value) clear_remarks_textarea: (uid) => ### * Clear textarea contents - * ### widget = @get_remarks_widget(uid) + return if widget is null el = widget.find('textarea') el.val("") @@ -84,6 +83,7 @@ class window.RemarksWidgetView * ### widget = @get_remarks_widget(uid) + return if widget is null return widget.find('.remarks_history').html() set_remarks: (value, uid) => @@ -97,6 +97,10 @@ class window.RemarksWidgetView .done (remarks) -> @update_remarks_history(remarks, uid) @clear_remarks_textarea(uid) + .fail -> + console.warn "Failed to get remarks" + .fail -> + console.warn "Failed to set remarks" ### ASYNC DATA METHODS ### @@ -106,8 +110,12 @@ class window.RemarksWidgetView ### deferred = $.Deferred() + widget = @get_remarks_widget(uid) + if widget is null + return deferred.reject() + fieldname = widget.attr("data-fieldname") @ajax_submit @@ -125,10 +133,15 @@ class window.RemarksWidgetView * Submit the value to the field setter via /@@API/update. * ### + deferred = $.Deferred() + widget = @get_remarks_widget(uid) + + if widget is null + return deferred.reject() + fieldname = widget.attr("data-fieldname") - deferred = $.Deferred() options = url: @get_portal_url() + "/@@API/update" data: diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.js b/bika/lims/skins/bika/bika_widgets/remarkswidget.js index 8e55544bbc..4c1c2daafb 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.js +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.js @@ -80,7 +80,6 @@ /* * Output HTML string. - * */ var remarks; remarks = value.replace(new RegExp("\n", "g"), "
"); @@ -91,10 +90,12 @@ /* * Clear and update the widget's History with the provided value. - * */ var el, widget; widget = this.get_remarks_widget(uid); + if (widget === null) { + return; + } el = widget.find('.remarks_history'); return el.html(this.format(value)); }; @@ -103,10 +104,12 @@ /* * Clear textarea contents - * */ var el, widget; widget = this.get_remarks_widget(uid); + if (widget === null) { + return; + } el = widget.find('textarea'); return el.val(""); }; @@ -120,6 +123,9 @@ */ var widget; widget = this.get_remarks_widget(uid); + if (widget === null) { + return; + } return widget.find('.remarks_history').html(); }; @@ -133,7 +139,11 @@ return this.fetch_remarks(uid).done(function(remarks) { this.update_remarks_history(remarks, uid); return this.clear_remarks_textarea(uid); + }).fail(function() { + return console.warn("Failed to get remarks"); }); + }).fail(function() { + return console.warn("Failed to set remarks"); }); }; @@ -148,6 +158,9 @@ var deferred, fieldname, widget; deferred = $.Deferred(); widget = this.get_remarks_widget(uid); + if (widget === null) { + return deferred.reject(); + } fieldname = widget.attr("data-fieldname"); this.ajax_submit({ url: this.get_portal_url() + "/@@API/read", @@ -169,9 +182,12 @@ * */ var deferred, fieldname, options, widget; + deferred = $.Deferred(); widget = this.get_remarks_widget(uid); + if (widget === null) { + return deferred.reject(); + } fieldname = widget.attr("data-fieldname"); - deferred = $.Deferred(); options = { url: this.get_portal_url() + "/@@API/update", data: { From 879f89439fdeea12923d538aa51a5384767fd91c Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 26 Jul 2018 14:24:20 +0200 Subject: [PATCH 09/21] Handle traceback on remarks field update --- bika/lims/browser/fields/remarksfield.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 978be6b957..35be63a14a 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -53,16 +53,16 @@ def get(self, instance, **kwargs): """Returns raw field value; the widget is responsible for converting from newlines to breaks, or other clever formatting. """ - value = ObjectField.get(self, instance, **kwargs) - # If field contains BaseUnit, then calling it returns the value. - if callable(value): - value = value() - return value + return self.getRaw(instance, **kwargs) def getRaw(self, instance, **kwargs): """Returns raw field value (possible wrapped in BaseUnit) """ value = ObjectField.get(self, instance, **kwargs) + # getattr(instance, "Remarks") returns a BaseUnit + if callable(value): + value = value() return value + registerField(RemarksField, title='Remarks', description='') From 9cedaae20078b1e3ff0ea6d2cc01e6a1864af28d Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 14:44:54 +0200 Subject: [PATCH 10/21] ResultsField.get() returns HTML --- bika/lims/browser/fields/remarksfield.py | 9 ++++++++- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 35be63a14a..44f9b0b03e 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -15,6 +15,13 @@ from zope.interface import implements +def format_remarks(text): + """Simple helper to convert plain text to HTML for Remarks rendering + """ + if not isinstance(text, basestring): + return "" + return text.replace('\n', '
') + class RemarksField(ObjectField): """A field that stores remarks. The value submitted to the setter will always be appended to the actual value of the field. @@ -53,7 +60,7 @@ def get(self, instance, **kwargs): """Returns raw field value; the widget is responsible for converting from newlines to breaks, or other clever formatting. """ - return self.getRaw(instance, **kwargs) + return format_remarks(self.getRaw(instance, **kwargs)) def getRaw(self, instance, **kwargs): """Returns raw field value (possible wrapped in BaseUnit) diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index 2d0d34d61c..f74f86f6cc 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -13,7 +13,7 @@ class="remarks_history"> textarea + tal:replace="structure history">textarea @@ -40,7 +40,7 @@
History - + content
From 6446cc86a42ecb7a5efbce64530e402db50b9079 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:13:05 +0200 Subject: [PATCH 11/21] Revert unrelated changes made in content sources --- bika/lims/content/artemplate.py | 228 +++++++++++++++----------------- bika/lims/content/pricelist.py | 47 +++---- bika/lims/content/worksheet.py | 142 +++++++++----------- 3 files changed, 190 insertions(+), 227 deletions(-) diff --git a/bika/lims/content/artemplate.py b/bika/lims/content/artemplate.py index 06055dd04c..a5f33bd4fe 100644 --- a/bika/lims/content/artemplate.py +++ b/bika/lims/content/artemplate.py @@ -11,40 +11,43 @@ """ from AccessControl import ClassSecurityInfo -from Products.ATExtensions.field.records import RecordsField from Products.Archetypes.public import * from Products.Archetypes.references import HoldingReference +from Products.CMFCore.permissions import View, ModifyPortalContent +from Products.ATExtensions.field.records import RecordsField from Products.CMFCore.utils import getToolByName from bika.lims import api -from bika.lims import bikaMessageFactory as _ +from bika.lims import PMF, bikaMessageFactory as _ from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import ARTemplateAnalysesWidget +from bika.lims.interfaces import IARTemplate +from bika.lims.browser.widgets import RecordsWidget as BikaRecordsWidget +from bika.lims.browser.widgets import RecordsWidget as RemarksWidget from bika.lims.browser.widgets import ARTemplatePartitionsWidget +from bika.lims.browser.widgets import ARTemplateAnalysesWidget +from bika.lims.browser.widgets import RecordsWidget from bika.lims.browser.widgets import ReferenceWidget -from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema -from bika.lims.interfaces import IARTemplate -from zope.interface import implements +from zope.interface import Interface, implements +import sys schema = BikaSchema.copy() + Schema(( - # SamplePoint and SampleType references are managed with - # accessors and mutators below to get/set a string value - # (the Title of the object), but still store a normal Reference. - # Form autocomplete widgets can then work with the Titles. - ReferenceField( - 'SamplePoint', - allowed_types=('SamplePoint',), - relationship='ARTemplateSamplePoint', - referenceClass=HoldingReference, - accessor='getSamplePoint', - edit_accessor='getSamplePoint', - mutator='setSamplePoint', + ## SamplePoint and SampleType references are managed with + ## accessors and mutators below to get/set a string value + ## (the Title of the object), but still store a normal Reference. + ## Form autocomplete widgets can then work with the Titles. + ReferenceField('SamplePoint', + vocabulary_display_path_bound = sys.maxint, + allowed_types = ('SamplePoint',), + relationship = 'ARTemplateSamplePoint', + referenceClass = HoldingReference, + accessor = 'getSamplePoint', + edit_accessor = 'getSamplePoint', + mutator = 'setSamplePoint', widget=ReferenceWidget( - label=_("Sample Point"), - description=_("Location where sample was taken"), - visible={'edit': 'visible', 'view': 'visible', - 'add': 'visible', + label = _("Sample Point"), + description = _("Location where sample was taken"), + visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, @@ -53,25 +56,23 @@ ), ComputedField( "SamplePointUID", - expression="context.Schema()['SamplePoint'].get(context) and " - "context.Schema()['SamplePoint'].get(context).UID() or ''", + expression="context.Schema()['SamplePoint'].get(context) and context.Schema()['SamplePoint'].get(context).UID() or ''", widget=ComputedWidget( visible=False, ), ), - ReferenceField( - 'SampleType', - allowed_types=('SampleType',), - relationship='ARTemplateSampleType', - referenceClass=HoldingReference, - accessor='getSampleType', - edit_accessor='getSampleType', - mutator='setSampleType', + ReferenceField('SampleType', + vocabulary_display_path_bound = sys.maxint, + allowed_types = ('SampleType',), + relationship = 'ARTemplateSampleType', + referenceClass = HoldingReference, + accessor = 'getSampleType', + edit_accessor = 'getSampleType', + mutator = 'setSampleType', widget=ReferenceWidget( - label=_("Sample Type"), - description=_("Create a new sample of this type"), - visible={'edit': 'visible', 'view': 'visible', - 'add': 'visible', + label = _("Sample Type"), + description = _("Create a new sample of this type"), + visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, @@ -80,14 +81,12 @@ ), ComputedField( "SampleTypeUID", - expression="context.Schema()['SampleType'].get(context) and " - "context.Schema()['SampleType'].get(context).UID() or ''", + expression="context.Schema()['SampleType'].get(context) and context.Schema()['SampleType'].get(context).UID() or ''", widget=ComputedWidget( visible=False, ), ), - BooleanField( - 'Composite', + BooleanField('Composite', default=False, widget=BooleanWidget( label=_("Composite"), @@ -101,97 +100,82 @@ label=_("Remarks"), ), ), - RecordsField( - 'Partitions', - schemata='Sample Partitions', - required=0, - type='artemplate_parts', - subfields=('part_id', - 'Container', - 'Preservation', - 'container_uid', - 'preservation_uid'), - subfield_labels={'part_id': _('Partition'), - 'Container': _('Container'), - 'Preservation': _('Preservation')}, - subfield_sizes={'part_id': 15, - 'Container': 35, - 'Preservation': 35}, - subfield_hidden={'preservation_uid': True, - 'container_uid': True}, - default=[{'part_id': 'part-1', - 'Container': '', - 'Preservation': '', - 'container_uid': '', - 'preservation_uid': ''}], + RecordsField('Partitions', + schemata = 'Sample Partitions', + required = 0, + type = 'artemplate_parts', + subfields = ('part_id', + 'Container', + 'Preservation', + 'container_uid', + 'preservation_uid'), + subfield_labels = {'part_id': _('Partition'), + 'Container': _('Container'), + 'Preservation': _('Preservation')}, + subfield_sizes = {'part_id': 15, + 'Container': 35, + 'Preservation': 35}, + subfield_hidden = {'preservation_uid': True, + 'container_uid': True}, + default = [{'part_id':'part-1', + 'Container':'', + 'Preservation':'', + 'container_uid':'', + 'preservation_uid':''}], widget=ARTemplatePartitionsWidget( - label=_("Sample Partitions"), - description=_( - "Configure the sample partitions and preservations " - + \ - "for this template. Assign analyses to the different " - "" + \ - "partitions on the template's Analyses tab"), + label = _("Sample Partitions"), + description = _("Configure the sample partitions and preservations " + \ + "for this template. Assign analyses to the different " + \ + "partitions on the template's Analyses tab"), combogrid_options={ 'Container': { 'colModel': [ - {'columnName': 'container_uid', - 'hidden': True}, - {'columnName': 'Container', 'width': '30', - 'label': _('Container')}, - {'columnName': 'Description', 'width': '70', - 'label': _('Description')}], + {'columnName':'container_uid', 'hidden':True}, + {'columnName':'Container', 'width':'30', 'label':_('Container')}, + {'columnName':'Description', 'width':'70', 'label':_('Description')}], 'url': 'getcontainers', 'showOn': True, 'width': '550px' }, 'Preservation': { 'colModel': [ - {'columnName': 'preservation_uid', - 'hidden': True}, - {'columnName': 'Preservation', 'width': '30', - 'label': _('Preservation')}, - {'columnName': 'Description', 'width': '70', - 'label': _('Description')}], + {'columnName':'preservation_uid', 'hidden':True}, + {'columnName':'Preservation', 'width':'30', 'label':_('Preservation')}, + {'columnName':'Description', 'width':'70', 'label':_('Description')}], 'url': 'getpreservations', 'showOn': True, 'width': '550px' }, }, - ), + ), ), - ReferenceField( - 'AnalysisProfile', - schemata='Analyses', - required=0, - multiValued=0, - allowed_types=('AnalysisProfile',), - relationship='ARTemplateAnalysisProfile', + ReferenceField('AnalysisProfile', + schemata = 'Analyses', + required = 0, + multiValued = 0, + allowed_types = ('AnalysisProfile',), + relationship = 'ARTemplateAnalysisProfile', widget=ReferenceWidget( - label=_("Analysis Profile"), - description=_( - "The Analysis Profile selection for this template"), - visible={'edit': 'visible', 'view': 'visible', - 'add': 'visible', + label = _("Analysis Profile"), + description =_("The Analysis Profile selection for this template"), + visible={'edit': 'visible', 'view': 'visible', 'add': 'visible', 'secondary': 'invisible'}, catalog_name='bika_setup_catalog', base_query={'inactive_state': 'active'}, showOn=True, ), ), - RecordsField( - 'Analyses', - schemata='Analyses', - required=0, - type='analyses', - subfields=('service_uid', 'partition'), - subfield_labels={'service_uid': _('Title'), - 'partition': _('Partition')}, - default=[], - widget=ARTemplateAnalysesWidget( - label=_("Analyses"), - description=_( - "Select analyses to include in this template"), + RecordsField('Analyses', + schemata = 'Analyses', + required = 0, + type = 'analyses', + subfields = ('service_uid', 'partition'), + subfield_labels = {'service_uid': _('Title'), + 'partition': _('Partition')}, + default = [], + widget = ARTemplateAnalysesWidget( + label = _("Analyses"), + description=_("Select analyses to include in this template"), ) ), # Custom settings for the assigned analysis services @@ -199,11 +183,10 @@ # Fields: # - uid: Analysis Service UID # - hidden: True/False. Hide/Display in results reports - RecordsField( - 'AnalysisServicesSettings', - required=0, - subfields=('uid', 'hidden',), - widget=ComputedWidget(visible=False), + RecordsField('AnalysisServicesSettings', + required=0, + subfields=('uid', 'hidden',), + widget=ComputedWidget(visible=False), ), ), ) @@ -214,7 +197,6 @@ # Update the validation layer after change the validator in runtime schema['title']._validationLayer() - class ARTemplate(BaseContent): security = ClassSecurityInfo() schema = schema @@ -222,24 +204,22 @@ class ARTemplate(BaseContent): implements(IARTemplate) _at_rename_after_creation = True - def _renameAfterCreation(self, check_auto_id=False): from bika.lims.idserver import renameAfterCreation renameAfterCreation(self) security.declarePublic('AnalysisProfiles') - def AnalysisProfiles(self, instance=None): instance = instance or self bsc = getToolByName(instance, 'bika_setup_catalog') items = [] for p in bsc(portal_type='AnalysisProfile', - inactive_state='active', - sort_on='sortable_title'): + inactive_state='active', + sort_on = 'sortable_title'): p = p.getObject() title = p.Title() items.append((p.UID(), title)) - items = [('', '')] + list(items) + items = [['','']] + list(items) return DisplayList(items) def getClientUID(self): @@ -248,6 +228,7 @@ def getClientUID(self): then that folder's UID must be returned in this index. """ return self.aq_parent.UID() + return '' def getAnalysisServiceSettings(self, uid): """ Returns a dictionary with the settings for the analysis @@ -255,8 +236,8 @@ def getAnalysisServiceSettings(self, uid): If there are no settings for the analysis service and template, returns a dictionary with the key 'uid' """ - sets = [s for s in self.getAnalysisServicesSettings() - if s.get('uid', '') == uid] + sets = [s for s in self.getAnalysisServicesSettings() \ + if s.get('uid','') == uid] return sets[0] if sets else {'uid': uid} def isAnalysisServiceHidden(self, uid): @@ -281,6 +262,7 @@ def isAnalysisServiceHidden(self, uid): raise ValueError('%s is not valid' % uid) return sets.get('hidden', False) + def remove_service(self, service): """Removes the service passed in from the services offered by the current Template. If the Analysis Service passed in is not assigned to @@ -294,8 +276,7 @@ def remove_service(self, service): # Remove the service from the referenced services services = self.getAnalyses() num_services = len(services) - services = [item for item in services if - item.get('service_uid', '') != uid] + services = [item for item in services if item.get('service_uid', '') != uid] removed = len(services) < num_services self.setAnalyses(services) @@ -306,5 +287,4 @@ def remove_service(self, service): return removed - registerType(ARTemplate, PROJECTNAME) diff --git a/bika/lims/content/pricelist.py b/bika/lims/content/pricelist.py index 91cfaaad57..bb060d3363 100644 --- a/bika/lims/content/pricelist.py +++ b/bika/lims/content/pricelist.py @@ -6,22 +6,27 @@ # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. from AccessControl import ClassSecurityInfo -from DateTime import DateTime -from Products.Archetypes.public import * -from Products.CMFCore import permissions from bika.lims import bikaMessageFactory as _ +from bika.lims.utils import t +from bika.lims.browser.widgets.datetimewidget import DateTimeWidget from bika.lims.browser.fields.remarksfield import RemarksField from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PRICELIST_TYPES, PROJECTNAME from bika.lims.content.bikaschema import BikaFolderSchema from bika.lims.interfaces import IPricelist +from DateTime import DateTime from persistent.mapping import PersistentMapping from plone.app.folder import folder +from Products.Archetypes.public import * +from Products.CMFCore import permissions from zope.interface import implements +from Products.CMFCore.utils import getToolByName +from Products.CMFCore import permissions + + schema = BikaFolderSchema.copy() + Schema(( - StringField( - 'Type', + StringField('Type', required=1, vocabulary=PRICELIST_TYPES, widget=SelectionWidget( @@ -29,22 +34,19 @@ label=_("Pricelist for"), ), ), - BooleanField( - 'BulkDiscount', + BooleanField('BulkDiscount', default=False, widget=SelectionWidget( label=_("Bulk discount applies"), ), ), - FixedPointField( - 'BulkPrice', + FixedPointField('BulkPrice', widget=DecimalWidget( label=_("Discount %"), description=_("Enter discount percentage value"), ), ), - BooleanField( - 'Descriptions', + BooleanField('Descriptions', default=False, widget=BooleanWidget( label=_("Include descriptions"), @@ -67,13 +69,13 @@ Field = schema['effectiveDate'] Field.schemata = 'default' -Field.required = 0 # "If no date is selected the item will be published -# immediately." +Field.required = 0 # "If no date is selected the item will be published + #immediately." Field.widget.visible = True Field = schema['expirationDate'] Field.schemata = 'default' -Field.required = 0 # "If no date is chosen, it will never expire." +Field.required = 0 # "If no date is chosen, it will never expire." Field.widget.visible = True @@ -110,11 +112,9 @@ def current_date(self): security.declareProtected(permissions.ModifyPortalContent, 'processForm') - registerType(Pricelist, PROJECTNAME) -# noinspection PyUnusedLocal def ObjectModifiedEventHandler(instance, event): """ Various types need automation on edit. """ @@ -126,16 +126,11 @@ def ObjectModifiedEventHandler(instance, event): """ # Remove existing line items instance.pricelist_lineitems = [] - for p in instance.portal_catalog( - portal_type=instance.getType(), inactive_state="active"): + for p in instance.portal_catalog(portal_type=instance.getType(), + inactive_state="active"): obj = p.getObject() itemDescription = None itemAccredited = False - price = '' - vat = '' - totalprice = '' - cat = '' - itemTitle = '' if instance.getType() == "LabProduct": print_detail = "" if obj.getVolume(): @@ -167,9 +162,9 @@ def ObjectModifiedEventHandler(instance, event): # cat = obj.getCategoryTitle() if instance.getBulkDiscount(): - price = float(obj.getBulkPrice()) - vat = get_vat_amount(price, obj.getVAT()) - totalprice = price + vat + price = float(obj.getBulkPrice()) + vat = get_vat_amount(price, obj.getVAT()) + totalprice = price + vat else: if instance.getBulkPrice(): discount = instance.getBulkPrice() diff --git a/bika/lims/content/worksheet.py b/bika/lims/content/worksheet.py index f32497c80b..f6ccc230af 100644 --- a/bika/lims/content/worksheet.py +++ b/bika/lims/content/worksheet.py @@ -9,43 +9,39 @@ import sys from AccessControl import ClassSecurityInfo -from DateTime import DateTime -from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin -from Products.ATExtensions.ateapi import RecordsField -from Products.Archetypes.config import REFERENCE_CATALOG -from Products.Archetypes.public import BaseFolder -from Products.Archetypes.public import DisplayList -from Products.Archetypes.public import ReferenceField -from Products.Archetypes.public import Schema -from Products.Archetypes.public import SelectionWidget -from Products.Archetypes.public import StringField -from Products.Archetypes.public import registerType -from Products.Archetypes.references import HoldingReference -from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import _createObjectByType, safe_unicode -from bika.lims import api, deprecated, logger from bika.lims import bikaMessageFactory as _ +from bika.lims import api, deprecated, logger from bika.lims.browser.fields import UIDReferenceField -from bika.lims.browser.fields.remarksfield import RemarksField -from bika.lims.browser.widgets import RemarksWidget from bika.lims.config import PROJECTNAME, WORKSHEET_LAYOUT_OPTIONS from bika.lims.content.bikaschema import BikaSchema from bika.lims.idserver import renameAfterCreation -from bika.lims.interfaces import IAnalysisRequest -from bika.lims.interfaces import IDuplicateAnalysis -from bika.lims.interfaces import IReferenceAnalysis -from bika.lims.interfaces import IReferenceSample -from bika.lims.interfaces import IRoutineAnalysis -from bika.lims.interfaces import IWorksheet -from bika.lims.permissions import EditWorksheet, ManageWorksheets +from bika.lims.browser.fields.remarksfield import RemarksField +from bika.lims.browser.widgets import RemarksWidget +from bika.lims.interfaces import (IAnalysisRequest, IDuplicateAnalysis, + IReferenceAnalysis, IReferenceSample, + IRoutineAnalysis, IWorksheet) from bika.lims.permissions import Verify as VerifyPermission -from bika.lims.utils import changeWorkflowState, tmpID, to_int +from bika.lims.permissions import EditWorksheet, ManageWorksheets from bika.lims.utils import to_utf8 as _c +from bika.lims.utils import changeWorkflowState, tmpID, to_int from bika.lims.workflow import doActionFor, getCurrentState, skip from bika.lims.workflow.worksheet import events, guards +from DateTime import DateTime from plone.api.user import has_permission +from Products.Archetypes.config import REFERENCE_CATALOG +from Products.Archetypes.public import (BaseFolder, DisplayList, + ReferenceField, Schema, + SelectionWidget, StringField, + TextAreaWidget, TextField, + registerType) +from Products.Archetypes.references import HoldingReference +from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin +from Products.ATExtensions.ateapi import RecordsField +from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.utils import _createObjectByType, safe_unicode from zope.interface import implements + ALL_ANALYSES_TYPES = "all" ALLOWED_ANALYSES_TYPES = ["a", "b", "c", "d"] @@ -69,8 +65,7 @@ 'Analyses', required=1, multiValued=1, - allowed_types=('Analysis', 'DuplicateAnalysis', 'ReferenceAnalysis', - 'RejectAnalysis'), + allowed_types=('Analysis', 'DuplicateAnalysis', 'ReferenceAnalysis', 'RejectAnalysis'), relationship='WorksheetAnalysis', ), @@ -187,15 +182,13 @@ def addAnalysis(self, analysis, position=None): self.setAnalyses(analyses + [analysis, ]) # if our parent has a position, use that one. - uids = [slot['container_uid'] for slot in layout] - if analysis.aq_parent.UID() in uids: + if analysis.aq_parent.UID() in [slot['container_uid'] for slot in layout]: position = [int(slot['position']) for slot in layout if slot['container_uid'] == analysis.aq_parent.UID()][0] else: # prefer supplied position parameter if not position: - positions = [int(slot['position']) for slot in layout] - used_positions = [0, ] + positions + used_positions = [0, ] + [int(slot['position']) for slot in layout] position = [pos for pos in range(1, max(used_positions) + 2) if pos not in used_positions][0] self.setLayout(layout + [{'position': position, @@ -316,7 +309,7 @@ def addReferenceAnalyses(self, reference, service_uids, dest_slot=None): refgid) if ref_analysis: - # All ref analyses from the same slot must have same group id + # All ref analyses from the same slot must have the same group id refgid = ref_analysis.getReferenceAnalysesGroupID() ref_analyses.append(ref_analysis) return ref_analyses @@ -552,7 +545,7 @@ def _get_suitable_slot_for_duplicate(self, src_slot): # those that belong to an Analysis Request. return 0 - occupied = self.get_slot_positions(typ='all') + occupied = self.get_slot_positions(type='all') wst = self.getWorksheetTemplate() if not wst: # No worksheet template assigned, add a new slot at the end of @@ -598,7 +591,7 @@ def _get_suitable_slot_for_references(self, reference): if not IReferenceSample.providedBy(reference): return 0 - occupied = self.get_slot_positions(typ='all') + occupied = self.get_slot_positions(type='all') wst = self.getWorksheetTemplate() if not wst: # No worksheet template assigned, add a new slot at the end of the @@ -704,7 +697,7 @@ def get_container_at(self, slot): return None - def get_slot_positions(self, typ=None): + def get_slot_positions(self, type='a'): """Returns a list with the slots occupied for the type passed in. Allowed type of analyses are: @@ -715,47 +708,44 @@ def get_slot_positions(self, typ=None): 'd' (duplicate) 'all' (all analyses) - :param typ: type of the analysis + :param type: type of the analysis :return: list of slot positions """ - typ = typ if typ else 'a' - if typ not in ALLOWED_ANALYSES_TYPES and typ != ALL_ANALYSES_TYPES: + if type not in ALLOWED_ANALYSES_TYPES and type != ALL_ANALYSES_TYPES: return list() layout = self.getLayout() slots = list() for pos in layout: - if typ != ALL_ANALYSES_TYPES and pos['type'] != typ: + if type != ALL_ANALYSES_TYPES and pos['type'] != type: continue slots.append(to_int(pos['position'])) # return a unique list of sorted slot positions return sorted(set(slots)) - def get_slot_position(self, container, typ=None): - """Returns the slot where the analyses from the type and container - passed + def get_slot_position(self, container, type='a'): + """Returns the slot where the analyses from the type and container passed in are located within the worksheet. :param container: the container in which the analyses are grouped - :param typ: type of the analysis + :param type: type of the analysis :return: the slot position :rtype: int """ - typ = typ if typ else 'a' - if not container or typ not in ALLOWED_ANALYSES_TYPES: + if not container or type not in ALLOWED_ANALYSES_TYPES: return None uid = api.get_uid(container) layout = self.getLayout() for pos in layout: - if pos['type'] != typ or pos['container_uid'] != uid: + if pos['type'] != type or pos['container_uid'] != uid: continue return to_int(pos['position']) return None - def resolve_available_slots(self, worksheet_template, typ=None): + def resolve_available_slots(self, worksheet_template, type='a'): """Returns the available slots from the current worksheet that fits with the layout defined in the worksheet_template and type of analysis passed in. @@ -768,20 +758,19 @@ def resolve_available_slots(self, worksheet_template, typ=None): 'd' (duplicate) :param worksheet_template: the worksheet template to match against - :param typ: type of analyses to restrict that suit with the slots + :param type: type of analyses to restrict that suit with the slots :return: a list of slots positions """ - typ = typ if typ else 'a' - if not worksheet_template or typ not in ALLOWED_ANALYSES_TYPES: + if not worksheet_template or type not in ALLOWED_ANALYSES_TYPES: return list() - ws_slots = self.get_slot_positions(typ) + ws_slots = self.get_slot_positions(type) layout = worksheet_template.getLayout() slots = list() for row in layout: # skip rows that do not match with the given type - if row['type'] != typ: + if row['type'] != type: continue slot = to_int(row['pos']) @@ -988,23 +977,23 @@ def _resolve_reference_sample(self, reference_samples=None, return best_sample, best_supported - def _resolve_reference_samples(self, wst, typ): + def _resolve_reference_samples(self, wst, type): """ Resolves the slots and reference samples in accordance with the Worksheet Template passed in and the type passed in. Returns a list of dictionaries :param wst: Worksheet Template that defines the layout - :param typ: type of analyses ('b' for blanks, 'c' for controls) + :param type: type of analyses ('b' for blanks, 'c' for controls) :return: list of dictionaries """ - if not typ or typ not in ['b', 'c']: + if not type or type not in ['b', 'c']: return [] bc = api.get_tool("bika_catalog") - wst_type = typ == 'b' and 'blank_ref' or 'control_ref' + wst_type = type == 'b' and 'blank_ref' or 'control_ref' slots_sample = list() - available_slots = self.resolve_available_slots(wst, typ) + available_slots = self.resolve_available_slots(wst, type) wst_layout = wst.getLayout() for row in wst_layout: slot = int(row['pos']) @@ -1029,8 +1018,8 @@ def _resolve_reference_samples(self, wst, typ): candidates = list() for sample in samples: obj = api.get_object(sample) - if (typ == 'b' and obj.getBlank()) or \ - (typ == 'c' and not obj.getBlank()): + if (type == 'b' and obj.getBlank()) or \ + (type == 'c' and not obj.getBlank()): candidates.append(obj) sample, uids = self._resolve_reference_sample(candidates, services) @@ -1043,22 +1032,21 @@ def _resolve_reference_samples(self, wst, typ): return slots_sample - def _apply_worksheet_template_reference_analyses(self, wst, typ=None): + def _apply_worksheet_template_reference_analyses(self, wst, type='all'): """ Add reference analyses to worksheet according to the worksheet template layout passed in. Does not overwrite slots that are already filled. :param wst: worksheet template used as the layout """ - typ = typ if typ else 'all' - if typ == 'all': + if type == 'all': self._apply_worksheet_template_reference_analyses(wst, 'b') self._apply_worksheet_template_reference_analyses(wst, 'c') return - if typ not in ['b', 'c']: + if type not in ['b', 'c']: return - references = self._resolve_reference_samples(wst, typ) + references = self._resolve_reference_samples(wst, type) for reference in references: slot = reference['slot'] sample = reference['sample'] @@ -1243,7 +1231,6 @@ def getNumberOfRegularSamples(self): security.declareProtected(EditWorksheet, 'resequenceWorksheet') - # noinspection PyUnusedLocal def resequenceWorksheet(self, REQUEST=None, RESPONSE=None): """ Reset the sequence of analyses in the worksheet """ """ sequence is [{'pos': , 'type': , 'uid', 'key'},] """ @@ -1355,7 +1342,6 @@ def setMethod(self, method, override_analyses=False): self.getField('Method').set(self, method) return total - # noinspection PyUnusedLocal @deprecated('[1703] Orphan. No alternative') def getFolderContents(self, contentFilter): """ @@ -1507,10 +1493,10 @@ def copy_src_fields_to_dst(src, dst): fieldname = field.getName() if fieldname in ignore_fields: continue - accessor = src.Schema().getField(fieldname).getAccessor(src) - mutator = dst.Schema().getField(fieldname).getMutator(dst) - getter = getattr(src, 'get' + fieldname, accessor) - setter = getattr(dst, 'set' + fieldname, mutator) + getter = getattr(src, 'get' + fieldname, + src.Schema().getField(fieldname).getAccessor(src)) + setter = getattr(dst, 'set' + fieldname, + dst.Schema().getField(fieldname).getMutator(dst)) if getter is None or setter is None: # ComputedField continue @@ -1546,11 +1532,12 @@ def copy_src_fields_to_dst(src, dst): new_ws_analyses = [] old_ws_analyses = [] for analysis in analyses: - position = analysis_positions[analysis.UID()] # Skip published or verified analyses review_state = workflow.getInfoFor(analysis, 'review_state', '') if review_state in ['published', 'verified', 'retracted']: old_ws_analyses.append(analysis.UID()) + + # XXX where does position come from? old_layout.append({'position': position, 'type': 'a', 'analysis_uid': analysis.UID(), @@ -1572,6 +1559,7 @@ def copy_src_fields_to_dst(src, dst): Retested=True, ) analysis.reindexObject() + position = analysis_positions[analysis.UID()] old_ws_analyses.append(reject.UID()) old_layout.append({'position': position, 'type': 'r', @@ -1589,8 +1577,9 @@ def copy_src_fields_to_dst(src, dst): service_uid = analysis.getServiceUID() reference = analysis.aq_parent reference_type = analysis.getReferenceType() - new_analysis_uid = reference.addReferenceAnalysis( - service_uid, reference_type) + new_analysis_uid = reference.addReferenceAnalysis(service_uid, + reference_type) + position = analysis_positions[analysis.UID()] old_ws_analyses.append(analysis.UID()) old_layout.append({'position': position, 'type': reference_type, @@ -1602,8 +1591,7 @@ def copy_src_fields_to_dst(src, dst): 'analysis_uid': new_analysis_uid, 'container_uid': reference.UID()}) workflow.doActionFor(analysis, 'reject') - brains = reference.uid_catalog(UID=new_analysis_uid) - new_reference = brains[0].getObject() + new_reference = reference.uid_catalog(UID=new_analysis_uid)[0].getObject() workflow.doActionFor(new_reference, 'assign') analysis.reindexObject() # Duplicate analyses @@ -1617,6 +1605,7 @@ def copy_src_fields_to_dst(src, dst): copy_src_fields_to_dst(analysis, new_duplicate) workflow.doActionFor(new_duplicate, 'assign') new_duplicate.reindexObject() + position = analysis_positions[analysis.UID()] old_ws_analyses.append(analysis.UID()) old_layout.append({'position': position, 'type': 'd', @@ -1636,8 +1625,7 @@ def copy_src_fields_to_dst(src, dst): for analysis in new_ws.getAnalyses(): review_state = workflow.getInfoFor(analysis, 'review_state', '') if review_state == 'to_be_verified': - changeWorkflowState( - analysis, "bika_analysis_workflow", "sample_received") + changeWorkflowState(analysis, "bika_analysis_workflow", "sample_received") self.REQUEST['context_uid'] = self.UID() self.setLayout(old_layout) self.setAnalyses(old_ws_analyses) From b7928fd9e96e10735a2d71effde427bd9ab2496b Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:27:37 +0200 Subject: [PATCH 12/21] RemarksWidget: Add some classes to form elements --- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index f74f86f6cc..ebbf335a46 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -21,7 +21,7 @@ - @@ -29,7 +29,7 @@ XXX Need some abstracted way to instruct the widget to hide the save button
- Date: Thu, 26 Jul 2018 15:32:02 +0200 Subject: [PATCH 13/21] Remove label "history" from RemarksWidget --- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 1 - 1 file changed, 1 deletion(-) diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index ebbf335a46..c5143f269c 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -39,7 +39,6 @@
- History content From 4709e18a2bd70f95268cb8acc80b4c20936ed939 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:36:23 +0200 Subject: [PATCH 14/21] Add remarks formatting directly to RemarksField.get --- bika/lims/browser/fields/remarksfield.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 44f9b0b03e..5f8693b2d7 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -15,13 +15,6 @@ from zope.interface import implements -def format_remarks(text): - """Simple helper to convert plain text to HTML for Remarks rendering - """ - if not isinstance(text, basestring): - return "" - return text.replace('\n', '
') - class RemarksField(ObjectField): """A field that stores remarks. The value submitted to the setter will always be appended to the actual value of the field. @@ -60,7 +53,8 @@ def get(self, instance, **kwargs): """Returns raw field value; the widget is responsible for converting from newlines to breaks, or other clever formatting. """ - return format_remarks(self.getRaw(instance, **kwargs)) + text = self.format_remarks(self.getRaw(instance, **kwargs)) + return text.replace('\n', '
') def getRaw(self, instance, **kwargs): """Returns raw field value (possible wrapped in BaseUnit) From 3022436426feb61ced944077c163c9e972b421e5 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:38:45 +0200 Subject: [PATCH 15/21] Add remarks formatting directly to RemarksField.get (redux) --- bika/lims/browser/fields/remarksfield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 5f8693b2d7..ae2922b09b 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -53,7 +53,7 @@ def get(self, instance, **kwargs): """Returns raw field value; the widget is responsible for converting from newlines to breaks, or other clever formatting. """ - text = self.format_remarks(self.getRaw(instance, **kwargs)) + text = self.getRaw(instance, **kwargs) return text.replace('\n', '
') def getRaw(self, instance, **kwargs): From 27b1ed0a41136ad3097405719c977bf456100dee Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:40:18 +0200 Subject: [PATCH 16/21] Prevent error in getter if RemarksField contains no value --- bika/lims/browser/fields/remarksfield.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index ae2922b09b..cb2aa624cf 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -54,6 +54,8 @@ def get(self, instance, **kwargs): from newlines to breaks, or other clever formatting. """ text = self.getRaw(instance, **kwargs) + if not text: + return "" return text.replace('\n', '
') def getRaw(self, instance, **kwargs): From ad04b5debf79635667e262dfdb040319245abc58 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:43:45 +0200 Subject: [PATCH 17/21] Remove misleading comment --- bika/lims/browser/fields/remarksfield.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index cb2aa624cf..074b26c2a6 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -50,8 +50,7 @@ def set(self, instance, value, **kwargs): return ObjectField.set(self, instance, remarks) def get(self, instance, **kwargs): - """Returns raw field value; the widget is responsible for converting - from newlines to breaks, or other clever formatting. + """Returns raw field value. """ text = self.getRaw(instance, **kwargs) if not text: From dde5c1d1f6570e697824d7e8518a07198ac288e8 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 15:47:07 +0200 Subject: [PATCH 18/21] Remove authenticator from RemarksWidget template --- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 1 - 1 file changed, 1 deletion(-) diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index c5143f269c..8962d8f6a8 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -48,7 +48,6 @@ - From 4c8bf0ae6aa2536b462d5de8e7a989d0b133e3f5 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 16:01:49 +0200 Subject: [PATCH 19/21] Add get_cooked_remarks to RemarksWidget... Standard getter returns unmodified text. --- bika/lims/browser/fields/remarksfield.py | 11 +++++++---- bika/lims/skins/bika/bika_widgets/remarkswidget.pt | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bika/lims/browser/fields/remarksfield.py b/bika/lims/browser/fields/remarksfield.py index 074b26c2a6..fe6669cc29 100644 --- a/bika/lims/browser/fields/remarksfield.py +++ b/bika/lims/browser/fields/remarksfield.py @@ -49,14 +49,17 @@ def set(self, instance, value, **kwargs): remarks = '\n'.join([divider, value, existing_remarks]) return ObjectField.set(self, instance, remarks) - def get(self, instance, **kwargs): - """Returns raw field value. - """ - text = self.getRaw(instance, **kwargs) + def get_cooked_remarks(self, instance): + text = self.get(instance) if not text: return "" return text.replace('\n', '
') + def get(self, instance, **kwargs): + """Returns raw field value. + """ + return self.getRaw(instance, **kwargs) + def getRaw(self, instance, **kwargs): """Returns raw field value (possible wrapped in BaseUnit) """ diff --git a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt index 8962d8f6a8..4c88bd9051 100644 --- a/bika/lims/skins/bika/bika_widgets/remarkswidget.pt +++ b/bika/lims/skins/bika/bika_widgets/remarkswidget.pt @@ -7,7 +7,7 @@ @@ -38,7 +38,7 @@ -
+
content From d9c41da8580eabd9d3967a997fb922d712b54580 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 16:14:29 +0200 Subject: [PATCH 20/21] Do not copy remarks when AR is copied to new AR. --- bika/lims/browser/analysisrequest/add2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bika/lims/browser/analysisrequest/add2.py b/bika/lims/browser/analysisrequest/add2.py index 6d8a570ee2..de45a39fc5 100644 --- a/bika/lims/browser/analysisrequest/add2.py +++ b/bika/lims/browser/analysisrequest/add2.py @@ -32,7 +32,7 @@ from zope.publisher.interfaces import IPublishTraverse AR_CONFIGURATION_STORAGE = "bika.lims.browser.analysisrequest.manage.add" -SKIP_FIELD_ON_COPY = ["Sample"] +SKIP_FIELD_ON_COPY = ["Sample", "Remarks"] def returns_json(func): From ce3c4fe4f38404ffc876ee167621e91bd3319660 Mon Sep 17 00:00:00 2001 From: Campbell Date: Thu, 26 Jul 2018 16:47:36 +0200 Subject: [PATCH 21/21] Update template condition to decide if Remarks field is shown --- .../analysisrequest/templates/analysisrequest_analyses.pt | 2 +- .../templates/analysisrequest_analyses_not_requested.pt | 2 +- .../analysisrequest/templates/analysisrequest_manage_results.pt | 2 +- .../browser/analysisrequest/templates/analysisrequest_view.pt | 2 +- bika/lims/browser/sample/templates/sample.pt | 2 +- bika/lims/browser/templates/pricelist_content.pt | 2 +- bika/lims/browser/templates/pricelist_email.pt | 2 +- bika/lims/browser/templates/referencesample_view.pt | 2 +- bika/lims/browser/worksheet/templates/results.pt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt index 764250d927..da02611f06 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_analyses.pt @@ -115,7 +115,7 @@ - - - - +

diff --git a/bika/lims/browser/templates/pricelist_email.pt b/bika/lims/browser/templates/pricelist_email.pt index 78732f1e92..8f9439d93e 100644 --- a/bika/lims/browser/templates/pricelist_email.pt +++ b/bika/lims/browser/templates/pricelist_email.pt @@ -19,7 +19,7 @@

- +

diff --git a/bika/lims/browser/templates/referencesample_view.pt b/bika/lims/browser/templates/referencesample_view.pt index fed7f373f5..f08c39f50e 100644 --- a/bika/lims/browser/templates/referencesample_view.pt +++ b/bika/lims/browser/templates/referencesample_view.pt @@ -139,7 +139,7 @@
- -