stats, optimiser le calcul du délai de traitement (#81734) #883
|
@ -1202,7 +1202,7 @@ def test_statistics_resolution_time(pub, freezer):
|
|||
assert resp.json['data'] == {
|
||||
'series': [
|
||||
{
|
||||
'data': [86400.0, 172800.0, 129600.0, 129600.0],
|
||||
'data': [86400, 172800, 129600, 129600],
|
||||
'label': 'Time between "New status" and any final status',
|
||||
}
|
||||
],
|
||||
|
@ -1297,10 +1297,70 @@ def test_statistics_resolution_time(pub, freezer):
|
|||
)
|
||||
assert resp.json['data']['series'][0]['data'] == []
|
||||
|
||||
# specify start status that is after end status
|
||||
resp = get_app(pub).get(
|
||||
sign_uri('/api/statistics/resolution-time/?form=test&start_status=4&end_status=2')
|
||||
)
|
||||
assert resp.json['data']['series'][0]['label'] == 'Time between "End status 2" and "Middle status"'
|
||||
assert get_humanized_duration_serie(resp.json) == []
|
||||
|
||||
# unknown form
|
||||
resp = get_app(pub).get(sign_uri('/api/statistics/resolution-time/?form=xxx'), status=400)
|
||||
|
||||
|
||||
def test_statistics_resolution_time_status_loop(pub, freezer):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
middle_status = workflow.add_status(name='Middle status')
|
||||
workflow.add_status(name='End status')
|
||||
|
||||
# add jump from new to middle, middle to new, and middle to end
|
||||
jump = new_status.add_action('jump', id='_jump')
|
||||
jump.status = '2'
|
||||
jump = middle_status.add_action('jump', id='_jump')
|
||||
jump.status = '1'
|
||||
jump = middle_status.add_action('jump', id='_jump')
|
||||
jump.status = '3'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
freezer.move_to(datetime.date(2021, 1, 1))
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
# one day after creation, jump to middle status
|
||||
freezer.move_to(datetime.date(2021, 1, 2))
|
||||
formdata.jump_status('2')
|
||||
formdata.store()
|
||||
|
||||
# two days after, jump to start status
|
||||
freezer.move_to(datetime.date(2021, 1, 4))
|
||||
formdata.jump_status('1')
|
||||
formdata.store()
|
||||
|
||||
# three days after, jump to middle status again
|
||||
freezer.move_to(datetime.date(2021, 1, 6))
|
||||
formdata.jump_status('2')
|
||||
formdata.store()
|
||||
|
||||
resp = get_app(pub).get(
|
||||
sign_uri('/api/statistics/resolution-time/?form=test&start_status=wf-new&end_status=2')
|
||||
)
|
||||
assert resp.json['data']['series'][0]['label'] == 'Time between "New status" and "Middle status"'
|
||||
# only first transition from new to middle is computed, later one is ignored
|
||||
assert get_humanized_duration_serie(resp.json) == [
|
||||
'1 day(s) and 0 hour(s)',
|
||||
'1 day(s) and 0 hour(s)',
|
||||
'1 day(s) and 0 hour(s)',
|
||||
'1 day(s) and 0 hour(s)',
|
||||
]
|
||||
|
||||
|
||||
def test_statistics_resolution_time_median(pub, freezer):
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
@ -1332,7 +1392,7 @@ def test_statistics_resolution_time_median(pub, freezer):
|
|||
resp = get_app(pub).get(sign_uri('/api/statistics/resolution-time/?form=test'))
|
||||
assert get_humanized_duration_serie(resp.json) == [
|
||||
'1 day(s) and 0 hour(s)', # min
|
||||
'89 day(s) and 22 hour(s)', # max
|
||||
'89 day(s) and 23 hour(s)', # max
|
||||
'13 day(s) and 23 hour(s)', # mean
|
||||
'5 day(s) and 0 hour(s)', # median
|
||||
]
|
||||
|
|
37
wcs/sql.py
37
wcs/sql.py
|
@ -2325,6 +2325,43 @@ class SqlDataMixin(SqlMixin):
|
|||
|
||||
cur.close()
|
||||
|
||||
@classmethod
|
||||
def get_resolution_times(cls, start_status, end_statuses, period_start=None, period_end=None):
|
||||
criterias = [StrictNotEqual('f.status', 'draft')]
|
||||
if period_start:
|
||||
criterias.append(GreaterOrEqual('f.receipt_time', period_start))
|
||||
if period_end:
|
||||
criterias.append(Less('f.receipt_time', period_end))
|
||||
|
||||
where_clauses, params, dummy = parse_clause(criterias)
|
||||
|
||||
params.update(
|
||||
{
|
||||
'start_status': start_status,
|
||||
'end_statuses': tuple(end_statuses),
|
||||
}
|
||||
)
|
||||
|
||||
table_name = cls._table_name
|
||||
sql_statement = f'''
|
||||
SELECT
|
||||
f.id,
|
||||
MIN(end_evo.time) - MIN(start_evo.time) as res_time
|
||||
FROM {table_name} f
|
||||
JOIN {table_name}_evolutions start_evo ON start_evo.formdata_id = f.id AND start_evo.status = %(start_status)s
|
||||
JOIN {table_name}_evolutions end_evo ON end_evo.formdata_id = f.id AND end_evo.status IN %(end_statuses)s
|
||||
WHERE {' AND '.join(where_clauses)}
|
||||
GROUP BY f.id
|
||||
ORDER BY res_time
|
||||
'''
|
||||
|
||||
|
||||
_, cur = get_connection_and_cursor()
|
||||
with cur:
|
||||
cur.execute(sql_statement, params)
|
||||
results = cur.fetchall()
|
||||
|
||||
return [res_time for row in results if (res_time := row[1].total_seconds()) > 0]
|
||||
|
||||
def _set_auto_fields(self, cur):
|
||||
if self.set_auto_fields():
|
||||
sql_statement = (
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
import time
|
||||
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
|
||||
from django.urls import reverse
|
||||
|
@ -32,7 +31,7 @@ from wcs.formdata import FormData
|
|||
from wcs.formdef import FormDef
|
||||
from wcs.qommon import _, misc, pgettext_lazy
|
||||
from wcs.qommon.errors import TraversalError
|
||||
from wcs.sql_criterias import Contains, Equal, GreaterOrEqual, Less, Null, Or, StrictNotEqual
|
||||
from wcs.sql_criterias import Contains, Equal, Null, Or, StrictNotEqual
|
||||
|
||||
|
||||
class RestrictedView(View):
|
||||
|
@ -713,17 +712,6 @@ class ResolutionTimeView(RestrictedView):
|
|||
]
|
||||
|
||||
def get_statistics(self, formdef):
|
||||
criterias = [StrictNotEqual('status', 'draft')]
|
||||
if self.request.GET.get('start'):
|
||||
criterias.append(GreaterOrEqual('receipt_time', self.request.GET['start']))
|
||||
if self.request.GET.get('end'):
|
||||
criterias.append(Less('receipt_time', self.request.GET['end']))
|
||||
|
||||
values = formdef.data_class().select(criterias)
|
||||
# load all evolutions in a single batch, to avoid as many query as
|
||||
# there are formdata when computing resolution times statistics.
|
||||
formdef.data_class().load_all_evolutions(values)
|
||||
|
||||
start_status = self.request.GET.get('start_status', formdef.workflow.possible_status[0].id)
|
||||
end_status = self.request.GET.get('end_status', 'done')
|
||||
|
||||
|
@ -749,22 +737,15 @@ class ResolutionTimeView(RestrictedView):
|
|||
'end_status': _('"%s"') % end_status.name if end_status != 'done' else _('any final status'),
|
||||
}
|
||||
|
||||
res_time_forms = []
|
||||
for filled in values:
|
||||
start_time = None
|
||||
for evo in filled.evolution or []:
|
||||
if start_status and evo.status == 'wf-%s' % start_status.id:
|
||||
start_time = time.mktime(evo.time)
|
||||
elif evo.status in end_statuses:
|
||||
if start_status and not start_time:
|
||||
break
|
||||
start_time = start_time or time.mktime(filled.receipt_time)
|
||||
res_time_forms.append(time.mktime(evo.time) - start_time)
|
||||
break
|
||||
res_time_forms = formdef.data_class().get_resolution_times(
|
||||
start_status='wf-%s' % start_status.id,
|
||||
end_statuses=end_statuses,
|
||||
period_start=self.request.GET.get('start'),
|
||||
period_end=self.request.GET.get('end'),
|
||||
)
|
||||
|
||||
if not res_time_forms:
|
||||
return label, []
|
||||
res_time_forms.sort()
|
||||
|
||||
sum_times = sum(res_time_forms)
|
||||
len_times = len(res_time_forms)
|
||||
|
|
Loading…
Reference in New Issue
Il me semble qu'il y aurait moyen de tout mettre dans la même f-string,
Je suis trop vieux pour penser à mettre les appels de fonctions à l'intérieur d'une chaîne de caractère :)