Welcome | Get started | Dive into Lino | Contribute | Topics | Reference | More
working
: Work time tracking¶
The lino_xl.lib.working
adds functionality for managing work time.
This page is a tested document and the following instructions are used for initialization:
>>> from lino import startup
>>> startup('lino_book.projects.noi1e.settings.demo')
>>> from lino.api.doctest import *
Note that the demo data is on fictive demo date May 22, 2015:
>>> dd.today()
datetime.date(2015, 5, 22)
The ticket_model
defines what a ticket actually
is. In Lino Noi this points to tickets.Ticket
. It can be any model that implements
Workable
.
Work sessions¶
A site manager can see all sessions of the demo project:
>>> rt.show(working.Sessions, limit=15)
...
===================================================== ========= ============ ============ ============ ========== ============ =============================== ========== ==================== =================== =====================
Ticket Worker Start date Start time End Date End Time Break Time Summary Duration Duration (decimal) Ticket # Subscription
----------------------------------------------------- --------- ------------ ------------ ------------ ---------- ------------ ------------------------------- ---------- -------------------- ------------------- ---------------------
#97 ('NoneType' object has no attribute 'isocode') Jean 22/05/2015 09:00:00 fiddle with get_auth() `#97 <Detail>`__ SLA 5/2014 (dde)
#109 ('NoneType' object has no attribute 'isocode') Jean 22/05/2015 09:00:00 0:10 jitsi meeting claire and paul `#109 <Detail>`__ SLA 3/2014 (aab)
#111 (Bars have no foo) Luc 22/05/2015 09:00:00 0:10 commit and push `#111 <Detail>`__
#9 (Foo never matches Bar) Luc 22/05/2015 09:00:00 empty recycle bin `#9 <Detail>`__
#73 ('NoneType' object has no attribute 'isocode') Mathieu 22/05/2015 09:00:00 0:10 meeting with john `#73 <Detail>`__ SLA 4/2014 (bcc)
#85 ('NoneType' object has no attribute 'isocode') Mathieu 22/05/2015 09:00:00 response to email `#85 <Detail>`__ SLA 2/2014 (welsch)
#97 ('NoneType' object has no attribute 'isocode') Mathieu 22/05/2015 09:00:00 check for comments `#97 <Detail>`__ SLA 5/2014 (dde)
#109 ('NoneType' object has no attribute 'isocode') Mathieu 22/05/2015 09:00:00 0:10 keep door open `#109 <Detail>`__ SLA 3/2014 (aab)
#99 (Bars have no foo) Luc 21/05/2015 12:58:00 21/05/2015 15:00:00 0:10 drive to brussels 1:52 1,87 `#99 <Detail>`__
#61 ('NoneType' object has no attribute 'isocode') Mathieu 21/05/2015 12:58:00 21/05/2015 13:10:00 catch the brown fox 0:12 0,20 `#61 <Detail>`__ SLA 1/2014 (welket)
#49 ('NoneType' object has no attribute 'isocode') Mathieu 21/05/2015 12:53:00 21/05/2015 12:58:00 brainstorming lou & paul 0:05 0,08 `#49 <Detail>`__ SLA 3/2014 (aab)
#87 (Bars have no foo) Luc 21/05/2015 12:48:00 21/05/2015 12:58:00 keep door open 0:10 0,17 `#87 <Detail>`__
#85 ('NoneType' object has no attribute 'isocode') Jean 21/05/2015 12:29:00 21/05/2015 13:06:00 peer review with mark 0:37 0,62 `#85 <Detail>`__ SLA 2/2014 (welsch)
#75 (Bars have no foo) Luc 21/05/2015 11:18:00 21/05/2015 12:48:00 check for comments 1:30 1,50 `#75 <Detail>`__
#73 ('NoneType' object has no attribute 'isocode') Jean 21/05/2015 09:00:00 21/05/2015 12:29:00 0:10 empty recycle bin 3:19 3,32 `#73 <Detail>`__ SLA 4/2014 (bcc)
**Total (2384 rows)** **7:45** **7,75**
===================================================== ========= ============ ============ ============ ========== ============ =============================== ========== ==================== =================== =====================
Some sessions are on private tickets:
>>> from django.db.models import Q
>>> rt.show(working.Sessions, column_names="ticket user duration", filter=Q(ticket__private=True))
...
============================== ======== ==========
Ticket Worker Duration
------------------------------ -------- ----------
#4 (⚒ Foo and bar don't baz) Luc 2:18
#12 (⚒ Foo cannot bar) Luc 1:30
**Total (2 rows)** **3:48**
============================== ======== ==========
Worked hours¶
Example of WorkedHours
table
>>> # rnd = settings.SITE.kernel.text_renderer
>>> rt.login('jean').show(working.WorkedHours)
...
===================================== ======================================================================== =========== ======= ====== ===========
Description Worked tickets Regular Extra Free Total
------------------------------------- ------------------------------------------------------------------------ ----------- ------- ------ -----------
`Friday, 22 May 2015 <Detail>`__ `#97 <Detail>`__, `#109 <Detail>`__ 0:02 0:02
`Thursday, 21 May 2015 <Detail>`__ `#73 <Detail>`__, `#85 <Detail>`__ 3:56 3:56
`Wednesday, 20 May 2015 <Detail>`__ `#25 <Detail>`__, `#61 <Detail>`__, `#37 <Detail>`__, `#49 <Detail>`__ 5:40 5:40
`Tuesday, 19 May 2015 <Detail>`__ `#1 <Detail>`__, `#115 <Detail>`__, `#13 <Detail>`__ 4:00 4:00
`Monday, 18 May 2015 <Detail>`__ `#91 <Detail>`__, `#103 <Detail>`__ 3:51 3:51
`Sunday, 17 May 2015 <Detail>`__ 0:00
`Saturday, 16 May 2015 <Detail>`__ 0:00
**Total (7 rows)** **17:29** **17:29**
===================================== ======================================================================== =========== ======= ====== ===========
In order to reproduce #523, let's ffind the users who worked on more than one site and then render this table to HTML.
>>> for u in users.User.objects.all():
... qs = tickets.Site.objects.filter(tickets_by_site__sessions_by_ticket__user=u).distinct()
... if qs.count() > 1:
... print("{} {} {}".format(str(u.username), "worked on", [o for o in qs]))
jean worked on [Site #1 ('pypi'), Site #4 ('security')]
luc worked on [Site #2 ('docs'), Site #5 ('cust')]
mathieu worked on [Site #1 ('pypi'), Site #4 ('security')]
>>> url = "/api/working/WorkedHours?"
>>> #url += "_dc=1583295523012&cw=398&cw=398&cw=76&cw=76&cw=76&cw=76&cw=281&cw=76&ch=&ch=&ch=&ch=&ch=&ch=&ch=true&ch=true&ci=detail_link&ci=worked_tickets&ci=vc0&ci=vc1&ci=vc2&ci=vc3&ci=description&ci=day_number&name=0&pv=188&pv=&pv=&pv=&lv=1583294270.8095002&an=show_as_html&sr="
>>> url += "an=show_as_html"
>>> test_client.force_login(rt.login('jean').user)
>>> res = test_client.get(url, REMOTE_USER="jean")
>>> json.loads(res.content.decode()) == {'open_url': '/bs3/working/WorkedHours?limit=15', 'success': True}
True
The html version of this table table has only 5 rows (4 data rows and the total row) because valueless rows are not included by default:
>>> ar = rt.login('jean')
>>> u = ar.get_user()
>>> ar = working.WorkedHours.request(user=u)
>>> ar = ar.spawn(working.WorkedHours)
>>> lst = list(ar)
>>> len(lst)
7
>>> e = ar.table2xhtml()
>>> len(e.findall('./tbody/tr'))
8
Service reports¶
A service report is a document used in various discussions with a stakeholder. It reports about the working time invested during a given date range. This report can serve as a base for writing invoices.
It can be addressed to a recipient (a user) and in that case will consider only the tickets for which this user has specified interest.
Database model: ServiceReport
.
A service report currently contains three tables:
a list of work sessions
a list of the tickets mentioned in the work sessions and their invested time
a list of sites mentioned in the work sessions and their invested time
Reporting type¶
The default ReportingTypes
implementation offers three choices.
>>> rt.show(working.ReportingTypes)
======= ========= ========= =========
value name text Tariff
------- --------- --------- ---------
10 regular Regular Regular
20 extra Extra Extra
30 free Free Free
======= ========= ========= =========
The local site admin can adapt above list to the site's needs. He also defines a default reporting type:
>>> dd.plugins.working.default_reporting_type
<working.ReportingTypes.regular:10>
Database models¶
- class lino_xl.lib.working.SessionType¶
Django model representing the type of a work session.
- class lino_xl.lib.working.Session¶
Django model representing a work session.
- start_date¶
The date when you started to work.
- start_time¶
The time (in hh:mm) when you started working on this session.
This is your local time according to the time zone specified in your preferences.
- end_date¶
Leave this field blank if it is the same date as start_date.
- end_time¶
The time (in hh:mm) when the worker stopped to work.
An empty
end_time
means that the user is still busy with that session, the session is not yet closed.end_session()
sets this to the current time.
- break_time¶
The time (in hh:mm) to remove from the duration resulting from the difference between
start_time
andend_time
.
- faculty¶
The faculty that has been used during this session. On a new session this defaults to the needed faculty currently specified on the ticket.
- site_ref¶
- duration¶
Virtual field returning the
computed_duration
.
- computed_duration¶
The duration of this session as a
Duration
.This is the mathematical difference between
start_time
andend_time
, minus thebreak_time
and the durations of sub-sessions of this session. (Details seelino_xl.lib.working.models.Session.compute_duration()
).
- duration_decimal¶
The
computed_duration
expressed as a decimal number rather than aDuration
. This format can be more convenient when processing the data in a spreadsheet.
- end_session()¶
Tell Lino that you stop this session for now. This will simply set the
end_time
to the current time.Implemented by
EndThisSession
.
Tables reference¶
- class lino_xl.lib.working.Sessions¶
- class lino_xl.lib.working.SessionsByTicket¶
The "Sessions" panel in the detail of a ticket.
- slave_summary¶
This panel shows:
- class lino_xl.lib.working.MySessions¶
Shows all my sessions.
Use the button to filter them. You can export them to Excel.
- class lino_xl.lib.working.MySessionsByDate¶
Shows my sessions of a given day.
Use this to manually edit your work sessions.
- class lino_xl.lib.working.SessionsByReport¶
- class lino_xl.lib.working.TicketsReport¶
- class lino_xl.lib.working.SitesByReport¶
The list of tickets mentioned in a service report.
- class lino_xl.lib.working.WorkersByReport¶
Actions reference¶
- class lino_xl.lib.working.StartTicketSession¶
The action behind
Workable.start_session
.
- class lino_xl.lib.working.EndSession¶
Common base for
EndThisSession
andEndTicketSession
.
- class lino_xl.lib.working.EndTicketSession¶
The action behind
Workable.end_session
.
- class lino_xl.lib.working.EndThisSession¶
The action behind
Session.end_session
.
The Workable model mixin¶
- class lino_xl.lib.working.Workable¶
Base class for things that workers can work on.
The model specified in
ticket_model
must be a subclass of this. For example, in Lino Noi tickets are workable.- is_workable_for()¶
Return True if the given user can start a work session on this object.
- on_worked()¶
This is automatically called when a work session has been created or modified.
- start_session()¶
Start a work session on this ticket.
See
StartTicketSession
.
- end_session()¶
Tell Lino that you stop working on this ticket for now. This will simply set the
Session.end_time
to the current time.Implemented by
EndTicketSession
.
Actions reference¶
- class lino_xl.lib.working.ShowMySessionsByDay¶
Shows your work sessions per day.
- class lino_xl.lib.working.TicketHasSessions¶
Select only tickets for which there has been at least one session during the given period.
This is added as item to
lino_xl.lib.tickets.TicketEvents
.
- class lino_xl.lib.working.ProjectHasSessions¶
Select only projects for which there has been at least one session during the given period.
This is added as item to
lino_xl.lib.tickets.ProjectEvents
.
- class lino_xl.lib.working.Worker¶
A user who is candidate for working on a ticket.
Summaries¶
- class lino_xl.lib.working.SiteSummary¶
An automatically generated record with yearly summary data about a site.
- class lino_xl.lib.working.SummariesBySite¶
Shows the summary records for a given site.
- class lino_xl.lib.working.UserSummary¶
An automatically generated record with monthly summary data about a user.
- class lino_xl.lib.working.SummariesByUser¶
Shows the summary records for a given user.
Examples¶
>>> rt.show(working.SiteSummaries, exclude=dict(regular_hours=""))
...
==== ====== ======= ========== ================ ================== ========= ======= =========
ID Year Month Project Active tickets Inactive tickets Regular Extra Free
---- ------ ------- ---------- ---------------- ------------------ --------- ------- ---------
2 2014 pypi 0 0 824:14
3 2015 pypi 0 0 434:30 681:06
5 2014 docs 0 0 427:30
6 2015 docs 0 0 232:39
11 2014 security 0 0 790:53
12 2015 security 0 0 447:26 1372:42
14 2014 cust 0 0 379:12
15 2015 cust 0 0 209:16
==== ====== ======= ========== ================ ================== ========= ======= =========
>>> rt.show(working.SiteSummaries)
...
==== ====== ======= ========== ================ ================== ========= ======= =========
ID Year Month Project Active tickets Inactive tickets Regular Extra Free
---- ------ ------- ---------- ---------------- ------------------ --------- ------- ---------
1 2013 pypi 0 0
2 2014 pypi 0 0 824:14
3 2015 pypi 0 0 434:30 681:06
4 2013 docs 0 0
5 2014 docs 0 0 427:30
6 2015 docs 0 0 232:39
7 2013 bugs 0 0
8 2014 bugs 0 0
9 2015 bugs 0 0
10 2013 security 0 0
11 2014 security 0 0 790:53
12 2015 security 0 0 447:26 1372:42
13 2013 cust 0 0
14 2014 cust 0 0 379:12
15 2015 cust 0 0 209:16
16 2013 admin 0 0
17 2014 admin 0 0
18 2015 admin 0 0
==== ====== ======= ========== ================ ================== ========= ======= =========
>>> rt.show(working.UserSummaries, exclude=dict(regular_hours=""))
...
===== ====== ====== ========= ========= ======= =========
ID Year Week User Regular Extra Free
----- ------ ------ --------- --------- ------- ---------
53 2014 1 Jean 13:31
68 2014 16 Jean 9:11
69 2014 17 Jean 21:55
70 2014 18 Jean 21:27
71 2014 19 Jean 23:07
72 2014 20 Jean 21:23
73 2014 21 Jean 21:18
74 2014 22 Jean 21:27
75 2014 23 Jean 23:07
76 2014 24 Jean 21:23
77 2014 25 Jean 21:18
78 2014 26 Jean 21:27
79 2014 27 Jean 23:07
80 2014 28 Jean 21:23
81 2014 29 Jean 21:18
82 2014 30 Jean 21:27
83 2014 31 Jean 23:07
84 2014 32 Jean 21:23
85 2014 33 Jean 21:18
86 2014 34 Jean 21:27
87 2014 35 Jean 23:07
88 2014 36 Jean 21:23
89 2014 37 Jean 21:18
90 2014 38 Jean 21:27
91 2014 39 Jean 23:07
92 2014 40 Jean 21:23
93 2014 41 Jean 21:18
94 2014 42 Jean 21:27
95 2014 43 Jean 23:07
96 2014 44 Jean 21:23
97 2014 45 Jean 21:18
98 2014 46 Jean 21:27
99 2014 47 Jean 23:07
100 2014 48 Jean 21:23
101 2014 49 Jean 21:18
102 2014 50 Jean 21:27
103 2014 51 Jean 23:07
104 2014 52 Jean 21:23
105 2015 1 Jean 7:47
106 2015 2 Jean 21:27
107 2015 3 Jean 23:52 1202:41
108 2015 4 Jean 22:08 851:07
109 2015 5 Jean 21:18
110 2015 6 Jean 21:27
111 2015 7 Jean 23:07
112 2015 8 Jean 21:23
113 2015 9 Jean 21:18
114 2015 10 Jean 21:27
115 2015 11 Jean 23:07
116 2015 12 Jean 21:23
117 2015 13 Jean 21:18
118 2015 14 Jean 21:27
119 2015 15 Jean 23:07
120 2015 16 Jean 21:23
121 2015 17 Jean 21:18
122 2015 18 Jean 21:27
123 2015 19 Jean 23:07
124 2015 20 Jean 21:23
125 2015 21 Jean 17:27
209 2014 1 Luc 11:47
224 2014 16 Luc 9:40
225 2014 17 Luc 21:23
226 2014 18 Luc 21:18
227 2014 19 Luc 21:27
228 2014 20 Luc 23:07
229 2014 21 Luc 21:23
230 2014 22 Luc 21:18
231 2014 23 Luc 21:27
232 2014 24 Luc 23:07
233 2014 25 Luc 21:23
234 2014 26 Luc 21:18
235 2014 27 Luc 21:27
236 2014 28 Luc 23:07
237 2014 29 Luc 21:23
238 2014 30 Luc 21:18
239 2014 31 Luc 21:27
240 2014 32 Luc 23:07
241 2014 33 Luc 21:23
242 2014 34 Luc 21:18
243 2014 35 Luc 21:27
244 2014 36 Luc 23:07
245 2014 37 Luc 21:23
246 2014 38 Luc 21:18
247 2014 39 Luc 21:27
248 2014 40 Luc 23:07
249 2014 41 Luc 21:23
250 2014 42 Luc 21:18
251 2014 43 Luc 21:27
252 2014 44 Luc 23:07
253 2014 45 Luc 21:23
254 2014 46 Luc 21:18
255 2014 47 Luc 21:27
256 2014 48 Luc 23:07
257 2014 49 Luc 21:23
258 2014 50 Luc 21:18
259 2014 51 Luc 21:27
260 2014 52 Luc 23:07
261 2015 1 Luc 9:36
262 2015 2 Luc 21:18
263 2015 3 Luc 21:27
264 2015 4 Luc 23:07
265 2015 5 Luc 21:23
266 2015 6 Luc 21:18
267 2015 7 Luc 21:27
268 2015 8 Luc 23:07
269 2015 9 Luc 21:23
270 2015 10 Luc 21:18
271 2015 11 Luc 21:27
272 2015 12 Luc 23:07
273 2015 13 Luc 21:23
274 2015 14 Luc 21:18
275 2015 15 Luc 21:27
276 2015 16 Luc 23:07
277 2015 17 Luc 21:23
278 2015 18 Luc 21:18
279 2015 19 Luc 21:27
280 2015 20 Luc 23:07
281 2015 21 Luc 17:27
521 2014 1 Mathieu 13:27
536 2014 16 Mathieu 7:51
537 2014 17 Mathieu 23:07
538 2014 18 Mathieu 21:23
539 2014 19 Mathieu 21:18
540 2014 20 Mathieu 21:27
541 2014 21 Mathieu 23:07
542 2014 22 Mathieu 21:23
543 2014 23 Mathieu 21:18
544 2014 24 Mathieu 21:27
545 2014 25 Mathieu 23:07
546 2014 26 Mathieu 21:23
547 2014 27 Mathieu 21:18
548 2014 28 Mathieu 21:27
549 2014 29 Mathieu 23:07
550 2014 30 Mathieu 21:23
551 2014 31 Mathieu 21:18
552 2014 32 Mathieu 21:27
553 2014 33 Mathieu 23:07
554 2014 34 Mathieu 21:23
555 2014 35 Mathieu 21:18
556 2014 36 Mathieu 21:27
557 2014 37 Mathieu 23:07
558 2014 38 Mathieu 21:23
559 2014 39 Mathieu 21:18
560 2014 40 Mathieu 21:27
561 2014 41 Mathieu 23:07
562 2014 42 Mathieu 21:23
563 2014 43 Mathieu 21:18
564 2014 44 Mathieu 21:27
565 2014 45 Mathieu 23:07
566 2014 46 Mathieu 21:23
567 2014 47 Mathieu 21:18
568 2014 48 Mathieu 21:27
569 2014 49 Mathieu 23:07
570 2014 50 Mathieu 21:23
571 2014 51 Mathieu 21:18
572 2014 52 Mathieu 21:27
573 2015 1 Mathieu 9:40
574 2015 2 Mathieu 21:23
575 2015 3 Mathieu 21:18
576 2015 4 Mathieu 21:27
577 2015 5 Mathieu 23:07
578 2015 6 Mathieu 21:23
579 2015 7 Mathieu 21:18
580 2015 8 Mathieu 21:27
581 2015 9 Mathieu 23:07
582 2015 10 Mathieu 21:23
583 2015 11 Mathieu 21:18
584 2015 12 Mathieu 21:27
585 2015 13 Mathieu 23:07
586 2015 14 Mathieu 21:23
587 2015 15 Mathieu 21:18
588 2015 16 Mathieu 21:27
589 2015 17 Mathieu 23:07
590 2015 18 Mathieu 21:23
591 2015 19 Mathieu 21:18
592 2015 20 Mathieu 21:27
593 2015 21 Mathieu 17:27
===== ====== ====== ========= ========= ======= =========
Some projects have more than 999:59 hours per year of work, which would be
indicated by a -1:00
if summaries.duration_max_length
was at its
default value of 6. But in lino_noi.lib.noi.settings
the default value is
modified to 10.
>>> dd.plugins.summaries.duration_max_length
10
>>> rt.login('jean').show(working.AllSummaries)
...
==== ====== ======= ========== ================ ================== ========= ======= =========
ID Year Month Project Active tickets Inactive tickets Regular Extra Free
---- ------ ------- ---------- ---------------- ------------------ --------- ------- ---------
1 2013 pypi 0 0
2 2014 pypi 0 0 824:14
3 2015 pypi 0 0 434:30 681:06
4 2013 docs 0 0
5 2014 docs 0 0 427:30
6 2015 docs 0 0 232:39
7 2013 bugs 0 0
8 2014 bugs 0 0
9 2015 bugs 0 0
10 2013 security 0 0
11 2014 security 0 0 790:53
12 2015 security 0 0 447:26 1372:42
13 2013 cust 0 0
14 2014 cust 0 0 379:12
15 2015 cust 0 0 209:16
16 2013 admin 0 0
17 2014 admin 0 0
18 2015 admin 0 0
==== ====== ======= ========== ================ ================== ========= ======= =========
Sub-sessions¶
To find the primary key of a session with sub-sessions, there is a print
statement in book/lino_book/projects/noi1e/settings/fixtures/demo.py
. So
if the following snippet fails, run go noi1e
followed by pm prep
and watch the output for two lines of text similar to the following ones:
20230117 Session 2373 has a subsession (compare docs/specs/working.rst)
20230117 Session 2379 has a subsession (compare docs/specs/working.rst)
And then use one of these numbers as the pk in the following snippet.
>>> obj = working.Session.objects.get(pk=2373)
>>> print(obj.break_time)
None
>>> st, et = obj.get_datetime('start'), obj.get_datetime('end')
>>> print("from {} to {}".format(st, et))
from 2015-01-14 09:00:00+00:00 to 2015-01-22 17:00:00+00:00
The session starts on 2015-02-14 at 09:00 and ends on 2015-01-22 at 17:00, so it lasts exactly 8 days and 8 hours, in other words 200 hours or 12000 minutes.
>>> delta = et - st
>>> print(delta)
8 days, 8:00:00
>>> print(delta.total_seconds()/3600)
200.0
>>> 8*24+8
200
But the computed_duration
is less than 200 hours:
>>> obj.computed_duration
Duration('168:17')
This is because there are sub-sessions. The get_sub_sessions()
method
iterates over them :
>>> from lino.utils.quantities import Duration
>>> for s in obj.get_sub_sessions():
... print("- {} {} ({})".format(s.computed_duration, s.break_time, s))
...
- 1:02 None (14.01.2015 09:00-10:02 Jean #67)
- 2:49 0:10 (14.01.2015 10:02-13:01 Jean #79)
- 2:58 0:10 (15.01.2015 09:00-12:53 Jean #91)
- 0:45 None (15.01.2015 09:30-10:15 Jean #43)
- 0:05 None (15.01.2015 12:53-12:58 Jean #103)
- 0:12 None (15.01.2015 12:58-13:10 Jean #115)
- 2:08 0:10 (16.01.2015 09:00-11:18 Jean #1)
- 1:30 None (16.01.2015 11:18-12:48 Jean #13)
- 0:10 None (16.01.2015 12:48-12:58 Jean #25)
- 1:52 0:10 (16.01.2015 12:58-15:00 Jean #37)
- 3:19 0:10 (19.01.2015 09:00-12:29 Jean #49)
- 0:37 None (19.01.2015 12:29-13:06 Jean #61)
- 1:02 None (20.01.2015 09:00-10:02 Jean #73)
- 0:45 None (20.01.2015 09:30-10:15 Jean #115)
- 2:49 0:10 (20.01.2015 10:02-13:01 Jean #85)
- 3:43 0:10 (21.01.2015 09:00-12:53 Jean #97)
- 0:05 None (21.01.2015 12:53-12:58 Jean #109)
- 0:12 None (21.01.2015 12:58-13:10 Jean #7)
- 2:08 0:10 (22.01.2015 09:00-11:18 Jean #19)
- 1:30 None (22.01.2015 11:18-12:48 Jean #31)
- 0:10 None (22.01.2015 12:48-12:58 Jean #43)
- 1:52 0:10 (22.01.2015 12:58-15:00 Jean #55)
Here is the sum of the durations of these sub-sessions:
>>> duration_of_sub_sessions = sum([s.computed_duration for s in obj.get_sub_sessions()])
>>> duration_of_sub_sessions
Duration('31:43')
And the sum of these two sums is indeed 200:00:
>>> duration_of_sub_sessions + obj.computed_duration
Duration('200:00')
Don't read me¶
>>> working.WorkedHours
lino_xl.lib.working.ui.WorkedHours
>>> print(working.WorkedHours.column_names)
detail_link worked_tickets vc0:5 vc1:5 vc2:5 vc3:5 *
>>> working.WorkedHours.get_data_elem('detail_link')
lino.core.actors.Actor.detail_link
Testing for equality of quantities¶
Remind the pitfall described in A possible pitfall. Here is a list of
sessions where the computed_duration
field is not exaclty the same as
the return value of compute_duration()
. They look the same when you
print them, but actually they differ.
>>> [obj.pk for obj in working.Session.objects.all()
... if obj.computed_duration != obj.compute_duration()]
...
[5, 6, 16, 17, ... 2382, 2383, 2384]
We verify this with the first one.
>>> obj = working.Session.objects.get(pk=5)
>>> obj.computed_duration
Duration('1:52')
>>> obj.compute_duration()
Duration('1:52')
But:
>>> obj.computed_duration == obj.compute_duration()
False
That's why the SessionChecker
must call str()
in order to get the
expected result:
>>> str(obj.computed_duration) == str(obj.compute_duration())
True