From 807dda3f415776de7b6bdc3912a63099457a7537 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 14:06:13 +0200 Subject: [PATCH 1/6] PEP8 only --- bika/lims/browser/analysisrequest/workflow.py | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/bika/lims/browser/analysisrequest/workflow.py b/bika/lims/browser/analysisrequest/workflow.py index 9b444c1036..511da0207f 100644 --- a/bika/lims/browser/analysisrequest/workflow.py +++ b/bika/lims/browser/analysisrequest/workflow.py @@ -5,36 +5,40 @@ # Copyright 2011-2017 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. -from bika.lims import bikaMessageFactory as _ -from bika.lims.utils import t -from bika.lims import PMF -from bika.lims.browser.bika_listing import WorkflowAction -from bika.lims.idserver import renameAfterCreation -from bika.lims.permissions import * -from bika.lims.utils import changeWorkflowState -from bika.lims.utils import encode_header -from bika.lims.utils import isActive -from bika.lims.utils import tmpID -from bika.lims.utils import to_utf8 -from bika.lims.workflow import doActionFor -from DateTime import DateTime +import json from string import Template + +from email.Utils import formataddr from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.Utils import formataddr -from Products.Archetypes.config import REFERENCE_CATALOG + +from DateTime import DateTime + from Products.Archetypes.event import ObjectInitializedEvent from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import safe_unicode, _createObjectByType -from bika.lims import interfaces +from Products.CMFPlone.utils import _createObjectByType +from Products.CMFPlone.utils import safe_unicode -import json import plone import zope.event +from bika.lims import PMF +from bika.lims import bikaMessageFactory as _ +from bika.lims import interfaces +from bika.lims.browser.bika_listing import WorkflowAction +from bika.lims.idserver import renameAfterCreation +from bika.lims.permissions import EditFieldResults +from bika.lims.permissions import EditResults +from bika.lims.permissions import PreserveSample +from bika.lims.utils import changeWorkflowState +from bika.lims.utils import encode_header +from bika.lims.utils import isActive +from bika.lims.utils import t +from bika.lims.utils import tmpID +from bika.lims.workflow import doActionFor + class AnalysisRequestWorkflowAction(WorkflowAction): - """Workflow actions taken in AnalysisRequest context. Sample context workflow actions also redirect here @@ -138,7 +142,7 @@ def workflow_action_save_analyses_button(self): for uid in Analyses: hidden = hiddenans.get(uid, '') hidden = True if hidden == 'on' else False - outs.append({'uid':uid, 'hidden':hidden}) + outs.append({'uid': uid, 'hidden': hidden}) ar.setAnalysisServicesSettings(outs) specs = {} @@ -248,13 +252,13 @@ def workflow_action_preserve(self): self.context.plone_utils.addPortalMessage(message, 'error') self.destination_url = self.request.get_header("referer", - self.context.absolute_url()) + self.context.absolute_url()) self.request.response.redirect(self.destination_url) def workflow_action_receive(self): action, came_from = WorkflowAction._get_form_workflow_action(self) - items = [self.context,] if came_from == 'workflow_action' \ - else self._get_selected_items().values() + items = [self.context, ] if came_from == 'workflow_action' \ + else self._get_selected_items().values() trans, dest = self.submitTransition(action, came_from, items) if trans and 'receive' in self.context.bika_setup.getAutoPrintStickers(): transitioned = [item.id for item in items] @@ -270,7 +274,6 @@ def workflow_action_receive(self): def workflow_action_submit(self): form = self.request.form - rc = getToolByName(self.context, REFERENCE_CATALOG) action, came_from = WorkflowAction._get_form_workflow_action(self) checkPermission = self.context.portal_membership.checkPermission if not isActive(self.context): @@ -361,14 +364,14 @@ def workflow_action_submit(self): # allow_setinstrument = sm.checkPermission(SetAnalysisInstrument) allow_setinstrument = True # ---8<----- - if allow_setinstrument == True: + if allow_setinstrument is True: # The current analysis allows the instrument regards # to its analysis service and method? - if (instruments[uid]==''): + if (instruments[uid] == ''): previnstr = analysis.getInstrument() if previnstr: previnstr.removeAnalysis(analysis) - analysis.setInstrument(None); + analysis.setInstrument(None) elif analysis.isInstrumentAllowed(instruments[uid]): previnstr = analysis.getInstrument() if previnstr: @@ -383,7 +386,7 @@ def workflow_action_submit(self): # allow_setmethod = sm.checkPermission(SetAnalysisMethod) allow_setmethod = True # ---8<----- - if allow_setmethod == True and analysis.isMethodAllowed(methods[uid]): + if allow_setmethod is True and analysis.isMethodAllowed(methods[uid]): analysis.setMethod(methods[uid]) # Need to save the analyst? @@ -507,7 +510,7 @@ def workflow_action_retract_ar(self): contact = ar.getContact() if contact: to.append(formataddr((encode_header(contact.Title()), - contact.getEmailAddress()))) + contact.getEmailAddress()))) for cc in ar.getCCContact(): formatted = formataddr((encode_header(cc.Title()), cc.getEmailAddress())) @@ -528,14 +531,9 @@ def workflow_action_retract_ar(self): ar.getRequestID()) naranchor = "%s" % (newar.absolute_url(), newar.getRequestID()) - addremarks = ('addremarks' in self.request - and ar.getRemarks()) \ - and ("

" - + _("Additional remarks:") - + "
" - + ar.getRemarks().split("===")[1].strip() - + "

") \ - or '' + addremarks = ('addremarks' in self.request and ar.getRemarks()) and ("

" + _("Additional remarks:") + + "
" + ar.getRemarks().split("===")[1].strip() + + "

") or '' sub_d = dict(request_link=aranchor, new_request_link=naranchor, remarks=addremarks, @@ -616,13 +614,13 @@ def cloneAR(self, ar): # retracted analyses won't be created/shown in the new AR workflow = getToolByName(self, "portal_workflow") analyses = [x for x in ans - if workflow.getInfoFor(x, "review_state") not in ("retracted")] + if workflow.getInfoFor(x, "review_state") not in ("retracted")] for an in analyses: try: nan = _createObjectByType("Analysis", newar, an.getKeyword()) except Exception as e: from bika.lims import logger - logger.warn('Cannot create analysis %s inside %s (%s)'% + logger.warn('Cannot create analysis %s inside %s (%s)' % an.getService().Title(), newar, e) continue nan.setService(an.getService()) From 5a2bb36cb08968fbb3725641a992a5a9eb706fbd Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 14:06:26 +0200 Subject: [PATCH 2/6] Overwrite num_tabs to avoid Schemata Selectbox conversion. --- bika/lims/skins/bika/form_tabbing.js | 185 +++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 bika/lims/skins/bika/form_tabbing.js diff --git a/bika/lims/skins/bika/form_tabbing.js b/bika/lims/skins/bika/form_tabbing.js new file mode 100644 index 0000000000..9153e99697 --- /dev/null +++ b/bika/lims/skins/bika/form_tabbing.js @@ -0,0 +1,185 @@ +/* + * This is the code for the tabbed forms. It assumes the following markup: + * + *
+ *
+ * Title + *
+ *
+ * + * or the following + * + *
+ *
Title
+ *
+ *
+ *
+ * + */ + + +/* Bika LIMS customizations + +Allow more than 6 Schemata before transforming into a selection box. +This is mainly done for the Bika Setup, which get's confusing to the user if the tabs are gone. + */ + +var ploneFormTabbing = { + // standard jQueryTools configuration options for all form tabs + jqtConfig:{current:'selected'}, + max_tabs: 8 // Allow more Schemata to be side-by-side + }; + + +(function($) { + +ploneFormTabbing._buildTabs = function(container, legends) { + var threshold = legends.length > ploneFormTabbing.max_tabs; + var panel_ids, tab_ids = [], tabs = ''; + + for (var i=0; i < legends.length; i++) { + var className, tab, legend = legends[i], lid = legend.id; + tab_ids[i] = '#' + lid; + + switch (i) { + case (0): + className = 'class="formTab firstFormTab"'; + break; + case (legends.length-1): + className = 'class="formTab lastFormTab"'; + break; + default: + className = 'class="formTab"'; + break; + } + + if (threshold) { + tab = ''; + } else { + tab = '
  • '; + tab += $(legend).text()+'
  • '; + } + + tabs += tab; + // don't use .hide() for ie6/7/8 support + $(legend).css({'visibility': 'hidden', 'font-size': '0', + 'padding': '0', 'height': '0', + 'width': '0', 'line-height': '0'}); + } + + tab_ids = tab_ids.join(','); + panel_ids = tab_ids.replace(/#fieldsetlegend-/g, "#fieldset-"); + + if (threshold) { + tabs = $(''); + tabs.change(function(){ + var selected = $(this).attr('value'); + $(this).parent().find('option#'+selected).click(); + }) + } else { + tabs = $(''); + } + + return tabs; +}; + + +ploneFormTabbing.initializeDL = function() { + var ftabs = $(ploneFormTabbing._buildTabs(this, $(this).children('dt'))); + var targets = $(this).children('dd'); + $(this).before(ftabs); + targets.addClass('formPanel'); + ftabs.tabs(targets, ploneFormTabbing.jqtConfig); +}; + + +ploneFormTabbing.initializeForm = function() { + var jqForm = $(this); + var fieldsets = jqForm.children('fieldset'); + + if (!fieldsets.length) {return;} + + var ftabs = ploneFormTabbing._buildTabs( + this, fieldsets.children('legend')); + $(this).prepend(ftabs); + fieldsets.addClass("formPanel"); + + + // The fieldset hidden may change, but is not content + $(this).find('input[name="fieldset"]').addClass('noUnloadProtection'); + + $(this).find('.formPanel:has(div.field span.required)').each(function() { + var id = this.id.replace(/^fieldset-/, "#fieldsetlegend-"); + $(id).addClass('required'); + }); + + // set the initial tab + var initialIndex = 0; + var count = 0; + var found = false; + $(this).find('.formPanel').each(function() { + if (!found && $(this).find('div.field.error').length!=0) { + initialIndex = count; + found = true; + } + count += 1; + }); + + var tabSelector = 'ul.formTabs'; + if ($(ftabs).is('select.formTabs')) { + tabSelector = 'select.formTabs'; + } + var tabsConfig = $.extend({}, ploneFormTabbing.jqtConfig, {'initialIndex':initialIndex}); + jqForm.children(tabSelector).tabs( + jqForm.children('fieldset.formPanel'), + tabsConfig + ); + + // save selected tab on submit + jqForm.submit(function() { + var selected; + if(ftabs.find('a.selected').length>=1){ + selected = ftabs.find('a.selected').attr('href').replace(/^#fieldsetlegend-/, "#fieldset-"); + } + else{ + selected = ftabs.attr('value').replace(/^fieldsetlegend-/,'#fieldset-'); + } + var fsInput = jqForm.find('input[name="fieldset"]'); + if (selected && fsInput) { + fsInput.val(selected); + } + }); + + $("#archetypes-schemata-links").addClass('hiddenStructure'); + $("div.formControls input[name='form.button.previous']," + + "div.formControls input[name='form.button.next']").remove(); + +}; + +$.fn.ploneTabInit = function(pbo) { + return this.each(function() { + var item = $(this); + + item.find("form.enableFormTabbing,div.enableFormTabbing").each(ploneFormTabbing.initializeForm); + item.find("dl.enableFormTabbing").each(ploneFormTabbing.initializeDL); + + //Select tab if it's part of the URL or designated in a hidden input + var targetPane = window.location.hash || item.find('.enableFormTabbing input[name="fieldset"]').val(); + if (targetPane) { + item.find('.enableFormTabbing .formTabs [id="' + + targetPane.replace('#','').replace('"', '').replace(/^fieldset-/, "fieldsetlegend-") + '"]').click(); + } + + }); +}; + +// initialize is a convenience function +ploneFormTabbing.initialize = function() { + $('body').ploneTabInit(); +}; + +})(jQuery); + +jQuery(function(){ploneFormTabbing.initialize();}); From d8ba1175eedc98cb09ef9261d56a226b24057b3b Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 14:09:42 +0200 Subject: [PATCH 3/6] Added boolean field to toggle notification settings on AR retract --- bika/lims/content/bikasetup.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/bika/lims/content/bikasetup.py b/bika/lims/content/bikasetup.py index fff5a03fbb..943ba970d3 100644 --- a/bika/lims/content/bikasetup.py +++ b/bika/lims/content/bikasetup.py @@ -723,7 +723,7 @@ class PrefixesField(RecordsField): ), BooleanField( 'NotifyOnRejection', - schemata="Analyses", + schemata="Notifications", default=False, widget=BooleanWidget( label=_("Email notification on rejection"), @@ -732,6 +732,17 @@ class PrefixesField(RecordsField): "Request is rejected.") ), ), + BooleanField( + 'NotifyOnARRetract', + schemata="Notifications", + default=True, + widget=BooleanWidget( + label=_("Email notification on AR retract"), + description=_("Select this to activate automatic notifications " + "via email to the Client and Lab Managers when an Analysis " + "Request is retracted.") + ), + ), BooleanField( 'AllowDepartmentFiltering', schemata="Security", From ab1cf75501b26b6b5186e0e27b3b1521fd75df2a Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 14:25:27 +0200 Subject: [PATCH 4/6] PEP8 only --- bika/lims/content/bikasetup.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bika/lims/content/bikasetup.py b/bika/lims/content/bikasetup.py index 943ba970d3..e4496d2644 100644 --- a/bika/lims/content/bikasetup.py +++ b/bika/lims/content/bikasetup.py @@ -425,13 +425,14 @@ class PrefixesField(RecordsField): "any Analysis in Analysis Service edit view. By default, 1"), ), ), - StringField('TypeOfmultiVerification', - schemata = "Analyses", - default = 'self_multi_enabled', - vocabulary = MULTI_VERIFICATION_TYPE, - widget = SelectionWidget( + StringField( + 'TypeOfmultiVerification', + schemata="Analyses", + default='self_multi_enabled', + vocabulary=MULTI_VERIFICATION_TYPE, + widget=SelectionWidget( label=_("Multi Verification type"), - description = _( + description=_( "Choose type of multiple verification for the same user." "This setting can enable/disable verifying/consecutively verifying" "more than once for the same user."), @@ -850,4 +851,5 @@ def _getNumberOfRequiredVerificationsVocabulary(self): items = [(1, '1'), (2, '2'), (3, '3'), (4, '4')] return IntDisplayList(list(items)) + registerType(BikaSetup, PROJECTNAME) From 4c3990ab8bc39816d51c78f3cddca2a6dc178c54 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 14:47:30 +0200 Subject: [PATCH 5/6] Implemented conditional email notification --- bika/lims/browser/analysisrequest/workflow.py | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/bika/lims/browser/analysisrequest/workflow.py b/bika/lims/browser/analysisrequest/workflow.py index 511da0207f..c09255b3dc 100644 --- a/bika/lims/browser/analysisrequest/workflow.py +++ b/bika/lims/browser/analysisrequest/workflow.py @@ -22,6 +22,7 @@ import plone import zope.event +from bika.lims import api from bika.lims import PMF from bika.lims import bikaMessageFactory as _ from bika.lims import interfaces @@ -64,6 +65,68 @@ def __call__(self): else: WorkflowAction.__call__(self) + def notify_ar_retract(self, ar, newar): + bika_setup = api.get_bika_setup() + laboratory = bika_setup.laboratory + lab_address = "
    ".join(laboratory.getPrintAddress()) + mime_msg = MIMEMultipart('related') + mime_msg['Subject'] = t(_("Erroneus result publication from ${request_id}", + mapping={"request_id": ar.getRequestID()})) + mime_msg['From'] = formataddr( + (encode_header(laboratory.getName()), + laboratory.getEmailAddress())) + to = [] + contact = ar.getContact() + if contact: + to.append(formataddr((encode_header(contact.Title()), + contact.getEmailAddress()))) + for cc in ar.getCCContact(): + formatted = formataddr((encode_header(cc.Title()), + cc.getEmailAddress())) + if formatted not in to: + to.append(formatted) + + managers = self.context.portal_groups.getGroupMembers('LabManagers') + for bcc in managers: + user = self.portal.acl_users.getUser(bcc) + if user: + uemail = user.getProperty('email') + ufull = user.getProperty('fullname') + formatted = formataddr((encode_header(ufull), uemail)) + if formatted not in to: + to.append(formatted) + mime_msg['To'] = ','.join(to) + aranchor = "%s" % (ar.absolute_url(), + ar.getRequestID()) + naranchor = "%s" % (newar.absolute_url(), + newar.getRequestID()) + addremarks = ('addremarks' in self.request and ar.getRemarks()) and ("

    " + _("Additional remarks:") + + "
    " + ar.getRemarks().split("===")[1].strip() + + "

    ") or '' + sub_d = dict(request_link=aranchor, + new_request_link=naranchor, + remarks=addremarks, + lab_address=lab_address) + body = Template("Some errors have been detected in the results report " + "published from the Analysis Request $request_link. The Analysis " + "Request $new_request_link has been created automatically and the " + "previous has been invalidated.
    The possible mistake " + "has been picked up and is under investigation.

    " + "$remarks $lab_address").safe_substitute(sub_d) + msg_txt = MIMEText(safe_unicode(body).encode('utf-8'), + _subtype='html') + mime_msg.preamble = 'This is a multi-part MIME message.' + mime_msg.attach(msg_txt) + try: + host = getToolByName(self.context, 'MailHost') + host.send(mime_msg.as_string(), immediate=True) + except Exception as msg: + message = _('Unable to send an email to alert lab ' + 'client contacts that the Analysis Request has been ' + 'retracted: ${error}', + mapping={'error': safe_unicode(msg)}) + self.context.plone_utils.addPortalMessage(message, 'warning') + def workflow_action_save_partitions_button(self): form = self.request.form # Sample Partitions or AR Manage Analyses: save Partition Table @@ -474,10 +537,10 @@ def workflow_action_verify(self): return self.workflow_action_default(action='verify', came_from=came_from) def workflow_action_retract_ar(self): - workflow = getToolByName(self.context, 'portal_workflow') + # AR should be retracted # Can't transition inactive ARs - if not isActive(self.context): + if not api.is_active(self.context): message = _('Item is inactive.') self.context.plone_utils.addPortalMessage(message, 'info') self.request.response.redirect(self.context.absolute_url()) @@ -488,7 +551,7 @@ def workflow_action_retract_ar(self): newar = self.cloneAR(ar) # 2. The old AR gets a status of 'invalid' - workflow.doActionFor(ar, 'retract_ar') + api.do_transition_for(ar, 'retract_ar') # 3. The new AR copy opens in status 'to be verified' changeWorkflowState(newar, 'bika_ar_workflow', 'to_be_verified') @@ -498,65 +561,9 @@ def workflow_action_retract_ar(self): # picked up and is under investigation. # A much possible information is provided in the email, linking # to the AR online. - laboratory = self.context.bika_setup.laboratory - lab_address = "
    ".join(laboratory.getPrintAddress()) - mime_msg = MIMEMultipart('related') - mime_msg['Subject'] = t(_("Erroneus result publication from ${request_id}", - mapping={"request_id": ar.getRequestID()})) - mime_msg['From'] = formataddr( - (encode_header(laboratory.getName()), - laboratory.getEmailAddress())) - to = [] - contact = ar.getContact() - if contact: - to.append(formataddr((encode_header(contact.Title()), - contact.getEmailAddress()))) - for cc in ar.getCCContact(): - formatted = formataddr((encode_header(cc.Title()), - cc.getEmailAddress())) - if formatted not in to: - to.append(formatted) - - managers = self.context.portal_groups.getGroupMembers('LabManagers') - for bcc in managers: - user = self.portal.acl_users.getUser(bcc) - if user: - uemail = user.getProperty('email') - ufull = user.getProperty('fullname') - formatted = formataddr((encode_header(ufull), uemail)) - if formatted not in to: - to.append(formatted) - mime_msg['To'] = ','.join(to) - aranchor = "%s" % (ar.absolute_url(), - ar.getRequestID()) - naranchor = "%s" % (newar.absolute_url(), - newar.getRequestID()) - addremarks = ('addremarks' in self.request and ar.getRemarks()) and ("

    " + _("Additional remarks:") + - "
    " + ar.getRemarks().split("===")[1].strip() + - "

    ") or '' - sub_d = dict(request_link=aranchor, - new_request_link=naranchor, - remarks=addremarks, - lab_address=lab_address) - body = Template("Some errors have been detected in the results report " - "published from the Analysis Request $request_link. The Analysis " - "Request $new_request_link has been created automatically and the " - "previous has been invalidated.
    The possible mistake " - "has been picked up and is under investigation.

    " - "$remarks $lab_address").safe_substitute(sub_d) - msg_txt = MIMEText(safe_unicode(body).encode('utf-8'), - _subtype='html') - mime_msg.preamble = 'This is a multi-part MIME message.' - mime_msg.attach(msg_txt) - try: - host = getToolByName(self.context, 'MailHost') - host.send(mime_msg.as_string(), immediate=True) - except Exception as msg: - message = _('Unable to send an email to alert lab ' - 'client contacts that the Analysis Request has been ' - 'retracted: ${error}', - mapping={'error': safe_unicode(msg)}) - self.context.plone_utils.addPortalMessage(message, 'warning') + bika_setup = api.get_bika_setup() + if bika_setup.getNotifyOnARRetract(): + self.notify_ar_retract(ar, newar) message = _('${items} invalidated.', mapping={'items': ar.getRequestID()}) From a9b9927e37f88d1f049b17517c5ca7c4070b02bc Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Mon, 11 Sep 2017 15:03:03 +0200 Subject: [PATCH 6/6] Changelog for #2204 --- docs/CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 2fe63457d7..852e1ed5d4 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -1,6 +1,7 @@ 3.3.0 (unreleased) ------------------ +- Issue-2204: Notification Emails on AR retract - Issue-2173: Publishing multiple ARs fails - Issue-2132: Reorderable Attachments for Reports - Issue-2129: Migrate Attachments to blobs