Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow refactoring: publish #144

Merged
merged 22 commits into from
Jun 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e70c779
Sanitize getTransitionDate from bika.lims.workflow
xispa Jun 11, 2017
1d9a679
Added getTransitionActor in bika.lims.workflow
xispa Jun 11, 2017
b7db80f
Added getDatePublished in AbstractRoutineAnalysis
xispa Jun 11, 2017
3861403
Use getTransitionDate and getTransitionActor
xispa Jun 11, 2017
4ef643c
Added getDateVerified and getStartProcessDate
xispa Jun 11, 2017
dfcc30c
Override getStartProcessDate in AbstractRoutineAnalysis
xispa Jun 11, 2017
554eebc
Add getEarliness and getDuration functions in AbstractAnalysis
xispa Jun 11, 2017
c38751a
Remove after_publish event associated to analysis
xispa Jun 11, 2017
af6479c
Publish transition to all children from analysis request
xispa Jun 11, 2017
da112a8
Remove condition "publish all analyses" from "workflow_skiplist"
xispa Jun 11, 2017
bf430f8
Remove Duration and Earliness fields from the Schema
xispa Jun 11, 2017
c23b77e
getEarliness returns negative if the analysis is late
xispa Jun 11, 2017
efadae5
Added isLateAnalysis function in AbstractAnalysis
xispa Jun 11, 2017
81a2d21
Remove workflow_script_publish from AbstractAnalysis
xispa Jun 12, 2017
e147df2
New TODO for Workflow stuff
xispa Jun 12, 2017
0e370c1
Merge branch 'wip' of https://github.com/naralabs/bika.lims into work…
xispa Jun 26, 2017
440a3b8
Added publish guard for Analysis Request
xispa Jun 26, 2017
7a40436
Add isBasicTransitionAllowed check in AR verify guard
xispa Jun 26, 2017
d784ae1
Comment the publish guard from analysis
xispa Jun 26, 2017
5e0af7d
Misspelling: poublic -> public
xispa Jun 26, 2017
e4f4ef0
Use publish guard instead of cancellation_object for Analysis Request
xispa Jun 26, 2017
9f184df
Added upgradestep 1707
xispa Jun 26, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 86 additions & 39 deletions bika/lims/content/abstractanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from bika.lims.utils.analysis import create_analysis, format_numeric_result
from bika.lims.utils.analysis import get_significant_digits
from bika.lims.workflow import doActionFor
from bika.lims.workflow import getTransitionActor
from bika.lims.workflow import getTransitionDate
from bika.lims.workflow import isBasicTransitionAllowed
from bika.lims.workflow import isTransitionAllowed
from bika.lims.workflow import wasTransitionPerformed
Expand Down Expand Up @@ -108,18 +110,6 @@
'DetectionLimitOperand'
)

# This is used to calculate turnaround time reports.
# The value is set when the Analysis is published.
Duration = IntegerField(
'Duration',
)

# This is used to calculate turnaround time reports. The value is set when the
# Analysis is published.
Earliness = IntegerField(
'Earliness',
)

# The ID of the logged in user who submitted the result for this Analysis.
Analyst = StringField(
'Analyst'
Expand Down Expand Up @@ -156,8 +146,6 @@
Calculation,
DateAnalysisPublished,
DetectionLimitOperand,
Duration,
Earliness,
# NumberOfRequiredVerifications overrides AbstractBaseClass
NumberOfRequiredVerifications,
Result,
Expand Down Expand Up @@ -617,6 +605,67 @@ def getTotalPrice(self):
"""
return Decimal(self.getPrice()) + Decimal(self.getVATAmount())

@security.public
def getDuration(self):
"""Returns the time in minutes taken for this analysis.
If the analysis is not yet 'ready to process', returns 0
If the analysis is still in progress (not yet verified),
duration = date_verified - date_start_process
Otherwise:
duration = current_datetime - date_start_process
:return: time in minutes taken for this analysis
:rtype: int
"""
starttime = self.getStartProcessDate()
if not starttime:
# The analysis is not yet ready to be processed
return 0

endtime = self.getDateVerified()
if not endtime:
# Assume here the analysis is still in progress, so use the current
# Date and Time
endtime = DateTime()

# Duration in minutes
duration = (endtime - starttime) * 24 * 60
return duration

@security.public
def getEarliness(self):
"""The remaining time in minutes for this analysis to be completed.
Returns zero if the analysis is neither 'ready to process' nor a
turnaround time is set.
earliness = duration - max_turnaround_time
The analysis is late if the earliness is negative
:return: the remaining time in minutes before the analysis reaches TAT
:rtype: int
"""
maxtime = self.getMaxTimeAllowed()
if not maxtime:
# No Turnaround time is set for this analysis
return 0
maxtime_delta = int(maxtime.get('days', 0)) * 86400
maxtime_delta += int(maxtime.get('hours', 0)) * 3600
maxtime_delta += int(maxtime.get('minutes', 0))
duration = self.getDuration()
earliness = maxtime_delta - duration
return earliness

def isLateAnalysis(self):
"""Returns true if the analysis is late in accordance with the maximum
turnaround time. If no maximum turnaround time is set for this analysis
or it is not yet ready to be processed, or there is still time
remaining (earliness), returns False.
:return: true if the analysis is late
:rtype: bool
"""
maxtime = self.getMaxTimeAllowed()
if not maxtime:
# No maximum turnaround time set, assume is not late
return False
return self.getEarliness() < 0

@security.public
def isInstrumentValid(self):
"""Checks if the instrument selected for this analysis is valid.
Expand Down Expand Up @@ -1050,32 +1099,35 @@ def getSubmittedBy(self):
state of the current analysis is "to_be_verified" or "verified"
:return: the user_id of the user who did the last submission of result
"""
workflow = getToolByName(self, "portal_workflow")
try:
review_history = workflow.getInfoFor(self, "review_history")
review_history = self.reverseList(review_history)
for event in review_history:
if event.get("action") == "submit":
return event.get("actor")
return ''
except WorkflowException:
return ''
return getTransitionActor(self, 'submit')

@security.public
def getDateSubmitted(self):
"""Returns the time the result was submitted.
:return: a DateTime object.
:rtype: DateTime
"""
workflow = getToolByName(self, "portal_workflow")
try:
review_history = workflow.getInfoFor(self, "review_history")
review_history = self.reverseList(review_history)
for event in review_history:
if event.get("action") == "submit":
return event.get("time")
return ''
except WorkflowException:
return ''
return getTransitionDate(self, 'submit', return_as_datetime=True)

@security.public
def getDateVerified(self):
"""Returns the time the analysis was verified. If the analysis hasn't
been yet verified, returns None
:return: the time the analysis was verified or None
:rtype: DateTime
"""
return getTransitionDate(self, 'verify', return_as_datetime=True)

@security.public
def getStartProcessDate(self):
"""Returns the date time when the analysis is ready to be processed.
It returns the datetime when the object was created, but might be
different depending on the type of analysis (e.g. "Date Received" for
routine analyses): see overriden functions.
:return: Date time when the analysis is ready to be processed.
:rtype: DateTime
"""
return self.created()

@security.public
def getParentUID(self):
Expand Down Expand Up @@ -1245,11 +1297,6 @@ def workflow_script_retract(self):
def workflow_script_verify(self):
events.after_verify(self)

@deprecated('[1705] Use bika.lims.workflow.analysis.events.after_publish')
@security.public
def workflow_script_publish(self):
events.after_publish(self)

@deprecated('[1705] Use bika.lims.workflow.analysis.events.after_cancel')
@security.public
def workflow_script_cancel(self):
Expand Down
18 changes: 18 additions & 0 deletions bika/lims/content/abstractroutineanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,31 @@ def getDateReceived(self):
"""
return getTransitionDate(self, 'receive', return_as_datetime=True)

@security.public
def getDatePublished(self):
"""Used to populate catalog values.
Returns the date on which the "publish" transition was invoked on this
analysis.
"""
return getTransitionDate(self, 'publish', return_as_datetime=True)

@security.public
def getDateSampled(self):
"""Used to populate catalog values.
Only has value when sampling_workflow is active.
"""
return getTransitionDate(self, 'sample', return_as_datetime=True)

@security.public
def getStartProcessDate(self):
"""Returns the date time when the analysis was received. If the
analysis hasn't yet been received, returns None
Overrides getStartProcessDateTime from the base class
:return: Date time when the analysis is ready to be processed.
:rtype: DateTime
"""
return self.getDateReceived()

@security.public
def getSamplePartitionUID(self):
part = self.getSamplePartition()
Expand Down
6 changes: 6 additions & 0 deletions bika/lims/content/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3143,6 +3143,12 @@ def guard_sample_prep_complete_transition(self):
def guard_schedule_sampling_transition(self):
return guards.schedule_sampling(self)

@deprecated('[1705] Use guards.publish from '
'bika.lims.workflow.analysisrequest')
@security.public
def guard_publish_transition(self):
return guards.publish(self)

@deprecated('[1705] Use events.after_no_sampling_workflow from '
'bika.lims.workflow.anaysisrequest')
@security.public
Expand Down
2 changes: 1 addition & 1 deletion bika/lims/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<metadata>
<version>3.2.0.1706</version>
<version>3.2.0.1707</version>
<dependencies>
<dependency>profile-jarn.jsi18n:default</dependency>
<dependency>profile-Products.ATExtensions:default</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ Target state:,published,,,,,,,,,,,,,,,
URL:,,,,,,,,,,,,,,,,
Trigger:,User,,,,,,,,,,,,,,,
Guard permission:,BIKA: Publish,,,,,,,,,,,,,,,
Guard expression:,python:here.guard_cancelled_object(),,,,,,,,,,,,,,,
Guard expression:,python:here.guard_publish_transition(),,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,
[Transition],,,,,,,,,,,,,,,,
Id:,receive,,,,,,,,,,,,,,,
Expand Down
2 changes: 1 addition & 1 deletion bika/lims/subscribers/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def ObjectInitializedEventHandler(instance, event):
return

def ObjectRemovedEventHandler(instance, event):

# TODO Workflow - Review all this function and normalize
# May need to promote the AR's review_state
# if all other analyses are at a higher state than this one was.
workflow = getToolByName(instance, 'portal_workflow')
Expand Down
7 changes: 7 additions & 0 deletions bika/lims/upgrade/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -604,4 +604,11 @@
destination="3.2.0.1706"
handler="bika.lims.upgrade.v3_2_0_1706.upgrade"
profile="bika.lims:default"/>

<genericsetup:upgradeStep
title="Upgrade to Bika LIMS 3.2.0.1707"
source="3.2.0.1706"
destination="3.2.0.1707"
handler="bika.lims.upgrade.v3_2_0_1707.upgrade"
profile="bika.lims:default"/>
</configure>
61 changes: 61 additions & 0 deletions bika/lims/upgrade/v3_2_0_1707.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# This file is part of Bika LIMS
#
# Copyright 2011-2017 by it's authors.
# Some rights reserved. See LICENSE.txt, AUTHORS.txt.
from Acquisition import aq_inner
from Acquisition import aq_parent

from bika.lims import logger
from bika.lims.upgrade import upgradestep
from bika.lims.upgrade.utils import UpgradeUtils
from plone.api.portal import get_tool

from Products.CMFCore.Expression import Expression

product = 'bika.lims'
version = '3.2.0.1707'


@upgradestep(product, version)
def upgrade(tool):
portal = aq_parent(aq_inner(tool))
ut = UpgradeUtils(portal)
ufrom = ut.getInstalledVersion(product)
if ut.isOlderVersion(product, version):
logger.info("Skipping upgrade of {0}: {1} > {2}".format(
product, ufrom, version))
# The currently installed version is more recent than the target
# version of this upgradestep
return True

logger.info("Upgrading {0}: {1} -> {2}".format(product, ufrom, version))

# Renames some guard expressions from several transitions
set_guard_expressions(portal)

logger.info("{0} upgraded to version {1}".format(product, version))
return True


def set_guard_expressions(portal):
"""Rename guard expressions of some workflow transitions
"""
logger.info('Renaming guard expressions...')
torename = {
'bika_ar_workflow.publish': 'python:here.guard_publish_transition()',
}
wtool = get_tool('portal_workflow')
workflowids = wtool.getWorkflowIds()
for wfid in workflowids:
workflow = wtool.getWorkflowById(wfid)
transitions = workflow.transitions
for transid in transitions.objectIds():
for torenid, newguard in torename.items():
tokens = torenid.split('.')
if tokens[0] == wfid and tokens[1] == transid:
transition = transitions[transid]
guard = transition.getGuard()
guard.expr = Expression(newguard)
transition.guard = guard
logger.info("Guard from transition '{0}' set to '{1}'"
.format(torenid, newguard))
25 changes: 20 additions & 5 deletions bika/lims/workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,19 +356,34 @@ def getCurrentState(obj, stateflowid='review_state'):
return wf.getInfoFor(obj, stateflowid, '')


def getTransitionActor(obj, action_id):
"""Returns the actor that performed a given transition. If transition has
not been perormed, or current user has no privileges, returns None
:return: the username of the user that performed the transition passed-in
:type: string
"""
review_history = getReviewHistory(obj)
for event in review_history:
if event.get('action') == action_id:
return event.get('actor')
return None


def getTransitionDate(obj, action_id, return_as_datetime=False):
"""
Returns date of action for object. Sometimes we need this date in Datetime
format and that's why added return_as_datetime param.
"""
review_history = getReviewHistory(obj)
for event in review_history:
if event['action'] == action_id:
if event.get('action') == action_id:
evtime = event.get('time')
if return_as_datetime:
return event['time']
value = ulocalized_time(event['time'], long_format=True,
time_only=False, context=obj)
return value
return evtime
if evtime:
value = ulocalized_time(evtime, long_format=True,
time_only=False, context=obj)
return value
return None


Expand Down
28 changes: 0 additions & 28 deletions bika/lims/workflow/analysis/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,34 +98,6 @@ def after_verify(obj):
doActionFor(ws, 'verify')


def after_publish(obj):
if skip(obj, "publish"):
return
workflow = getToolByName(obj, "portal_workflow")
state = workflow.getInfoFor(obj, 'cancellation_state', 'active')
if state == "cancelled":
return False
endtime = DateTime()
obj.setDateAnalysisPublished(endtime)
starttime = obj.aq_parent.getDateReceived()
starttime = starttime or obj.created()
maxtime = obj.getMaxTimeAllowed()
# set the instance duration value to default values
# in case of no calendars or max hours
if maxtime:
duration = (endtime - starttime) * 24 * 60
maxtime_delta = int(maxtime.get("hours", 0)) * 86400
maxtime_delta += int(maxtime.get("hours", 0)) * 3600
maxtime_delta += int(maxtime.get("minutes", 0)) * 60
earliness = duration - maxtime_delta
else:
earliness = 0
duration = 0
obj.setDuration(duration)
obj.setEarliness(earliness)
obj.reindexObject()


def after_cancel(obj):
if skip(obj, "cancel"):
return
Expand Down
Loading