stats, optimiser le calcul du délai de traitement (#81734) #883

Merged
vdeniaud merged 3 commits from wip/81734-vdeniaud into main 2023-11-30 16:42:48 +01:00
3 changed files with 106 additions and 28 deletions

View File

@ -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
]

View File

@ -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
'''

Il me semble qu'il y aurait moyen de tout mettre dans la même f-string,

...
WHERE {' AND '.join(where_clauses)}
GROUP BY f.id
ORDER BY res_time
Il me semble qu'il y aurait moyen de tout mettre dans la même f-string, ``` ... WHERE {' AND '.join(where_clauses)} GROUP BY f.id ORDER BY res_time ```

Je suis trop vieux pour penser à mettre les appels de fonctions à l'intérieur d'une chaîne de caractère :)

Je suis trop vieux pour penser à mettre les appels de fonctions à l'intérieur d'une chaîne de caractère :)
_, 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 = (

View File

@ -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)