Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
tickets
in Noi¶
The lino_noi.lib.tickets
plugin extends lino_xl.lib.tickets
to
make it collaborate with lino_noi.lib.working
.
In Lino Noi the site of a ticket also indicates “who is going to pay” for our work. Lino Noi uses this information when generating a service report.
This page contains code snippets (lines starting with >>>
), which are
being tested during our development workflow. The following
snippet initializes the demo project used throughout this page.
>>> from lino_book.projects.noi1e.startup import *
Tickets¶
Here is a textual description of the fields and their layout used in the detail window of a ticket.
>>> from lino.utils.diag import py2rst
>>> print(py2rst(tickets.AllTickets.detail_layout, True))
...
(main) [visible for all]:
- **General** (general_tab_1):
- **None** (overview)
- (general3):
- (general3_1): **Workflow** (workflow_buttons), **Assign to** (quick_assign_to) [visible for developer admin]
- **Comments** (comments.CommentsByRFC)
- **Request** (post_tab_1):
- (post1):
- (post1_1): **ID** (id), **Author** (user)
- **Summary** (summary)
- **Description** (description)
- (post2):
- (post2_1): **Subscription** (order), **End user** (end_user)
- (post2_2): **Confidential** (private), **Urgent** (urgent)
- **Upload files** (uploads.UploadsByController) [visible for customer contributor developer admin]
- **Triage** (triage_tab_1):
- (triage1):
- (triage1_1): **Team** (group), **Ticket type** (ticket_type)
- **Parent** (parent)
- **Children** (tickets.TicketsByParent)
- (triage2): **Assign to** (quick_assign_to) [visible for developer admin], **Add tag** (add_tag), **Tags** (topics.TagsByOwner) [visible for customer contributor developer admin]
- **Work** (work_tab_1):
- (work1):
- **Workflow** (workflow_buttons)
- (work1_2): **Priority** (priority), **My nickname** (my_nickname)
- **Mentioned in** (comments.CommentsByMentioned)
- (work2):
- **Working sessions** (working.SessionsByTicket) [visible for contributor developer admin]
- (work2_2): **Regular** (regular_hours), **Free** (free_hours)
- **More** (more_tab_1):
- (more1):
- (more1_1): **Created** (created), **Modified** (modified)
- **Reference** (ref)
- **Resolution** (upgrade_notes)
- (more2):
- (more2_1): **State** (state), **Assigned to** (assigned_to)
- (more2_2): **Planned time** (planned_time), **Deadline** (deadline)
- **Duplicate of** (duplicate_of)
- **Duplicates** (tickets.DuplicatesByTicket)
Screenshots¶

The life cycle of a ticket¶
In Lino Noi we use the default tickets workflow defined in
lino_xl.lib.tickets.TicketStates
.
Subscriptions¶
The Ticket.order
field in Noi points to a
lino_xl.lib.subscriptions.Subscription
.
>>> rt.show(subscriptions.Subscriptions)
==== ============ =========== ===================== ========= ================
ID Start date Reference Partner Subject Workflow
---- ------------ ----------- --------------------- --------- ----------------
1 07/01/2014 welket Rumma & Ko OÜ **Registered**
2 27/01/2014 welsch Bäckerei Ausdemwald **Registered**
3 16/02/2014 aab Bäckerei Mießen **Registered**
4 08/03/2014 bcc Bäckerei Schmitz **Registered**
5 28/03/2014 dde Garage Mergelsberg **Registered**
==== ============ =========== ===================== ========= ================
Ticket types¶
The demo
fixture defines the following ticket types.
>>> rt.show(tickets.TicketTypes)
============= ================== ================== ================
Designation Designation (de) Designation (fr) Reporting type
------------- ------------------ ------------------ ----------------
Bugfix Bugfix Bugfix Regular
Enhancement Enhancement Enhancement Regular
Upgrade Upgrade Upgrade Regular
Regression Regression Regression Free
============= ================== ================== ================
Deciding what to do next¶
Show all active tickets reported by me.
>>> rt.login('marc').get_user().user_type
<users.UserTypes.customer:100>
Marc is a customer, so he has permission to modify the workflow state of his own tickets but not of those owned by other users. He can have tickets to do, i.e. that are assiged to him.
TODO: The following 2 snippets are skipped because MyTicketsToWork sorts on
[“-priority”, “-modified”], and the value of modified
is unreliable.
Seems that it gets modified by some other test cases, which is not detected by
lino.utils.dbhash.check_virgin()
.
>>> rt.login('marc').show(tickets.MyTicketsToWork, max_width=40, display_mode="grid")
...
+----------+------------------------------------------+------------------------------------------+
| Priority | Ticket | Workflow |
+==========+==========================================+==========================================+
| 50 | `#2 (Bar is not always baz) <…>`__ ( 50 | **🗪 Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] |
| | by `Marc <…>`__ in `Sales team <…>`__ | [⧖] |
| | assigned to `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
| 50 | `#11 (Class-based Foos and Bars?) <…>`__ | **🗪 Talk** |
| | ( 50 by `Rolf Rompen <…>`__ in `Sales | |
| | team <…>`__ assigned to `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
| 30 | `#92 (Why <p> tags are so bar) <…>`__ | **🗪 Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] |
| | (by `Marc <…>`__ in `Sales team <…>`__ | [⧖] |
| | assigned to `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
| 30 | `#74 (Bar cannot baz) <…>`__ (by `Marc | **🗪 Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] |
| | <…>`__ in `Sales team <…>`__ assigned to | [⧖] |
| | `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
| 30 | `#83 (Misc optimizations in Baz) <…>`__ | **🗪 Talk** |
| | (by `Rolf Rompen <…>`__ in `Sales team | |
| | <…>`__ assigned to `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
| 30 | `#20 (Why <p> tags are so bar) <…>`__ | **🗪 Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] |
| | (by `Marc <…>`__ in `Sales team <…>`__ | [⧖] |
| | assigned to `Marc <…>`__) | |
+----------+------------------------------------------+------------------------------------------+
>>> rt.login('jean').show(tickets.MyTickets, max_width=30, display_mode="grid")
...
+----------+--------------------------------+-------------+--------------------------------+
| Priority | Ticket | Assigned to | Workflow |
+==========+================================+=============+================================+
| 50 | `#12 (Foo cannot bar) <…>`__ ( | Robin Rood | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | 50 in `Developers <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | assigned to `Robin Rood | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 40 | `#3 (Baz sucks) <…>`__ ( 40 in | Jean | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | `Developers <…>`__ assigned to | | [⚒] [☐] [☑] [☒] [⧖] |
| | `Jean <…>`__) [▶] **☉ Open** → | | |
| | [⚹] [☾] [🗪] [⚒] [☐] [☑] [☒] | | |
| | [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#111 (Bars have no foo) | Jean | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | <…>`__ (in `Developers <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | assigned to `Jean <…>`__) [▶] | | |
| | **☉ Open** → [⚹] [☾] [🗪] [⚒] | | |
| | [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#102 (No more foo when bar is | Mathieu | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | gone) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__ assigned to `Mathieu | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#93 (Irritating message when | | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | bar) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#84 (Default account in | Robin Rood | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | invoices per partner) <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | (in `Developers <…>`__ | | |
| | assigned to `Robin Rood | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#75 (Bars have no foo) <…>`__ | Jean | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | (in `Developers <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | assigned to `Jean <…>`__) [▶] | | |
| | **☉ Open** → [⚹] [☾] [🗪] [⚒] | | |
| | [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#66 (No more foo when bar is | Mathieu | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | gone) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__ assigned to `Mathieu | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#57 (Irritating message when | | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | bar) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#48 (Default account in | Robin Rood | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | invoices per partner) <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | (in `Developers <…>`__ | | |
| | assigned to `Robin Rood | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#39 (Bars have no foo) <…>`__ | Jean | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | (in `Developers <…>`__ | | [⚒] [☐] [☑] [☒] [⧖] |
| | assigned to `Jean <…>`__) [▶] | | |
| | **☉ Open** → [⚹] [☾] [🗪] [⚒] | | |
| | [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#30 (No more foo when bar is | Mathieu | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | gone) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__ assigned to `Mathieu | | |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
| 30 | `#21 (Irritating message when | | [▶] **☉ Open** → [⚹] [☾] [🗪] |
| | bar) <…>`__ (in `Developers | | [⚒] [☐] [☑] [☒] [⧖] |
| | <…>`__) [▶] **☉ Open** → [⚹] | | |
| | [☾] [🗪] [⚒] [☐] [☑] [☒] [⧖] | | |
+----------+--------------------------------+-------------+--------------------------------+
The backlog¶
The TicketsByOrder
panel shows all the tickets for a given
subscription. It is a scrum backlog.
>>> pypi = subscriptions.Subscription.objects.get(ref="welket")
>>> rt.login("robin").show(
... tickets.TicketsByOrder, pypi, max_width=40, display_mode="grid")
...
+--------------------+------------------------------------------+--------------+------------+-----------+
| Priority | Ticket | Planned time | Regular | Free |
+====================+==========================================+==============+============+===========+
| 50 | `#1 (Föö fails to bar when baz) <…>`__ ( | | 34:44 | |
| | 50 by `Luc <…>`__ in `Managers <…>`__) | | | |
| | [▶] **⚹ New** → [☾] [🗪] [☉] [⚒] [☐] [☑] | | | |
| | [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#91 (Cannot delete foo) <…>`__ (by `Luc | | 31:23 | |
| | <…>`__ in `Managers <…>`__ assigned to | | | |
| | `Luc <…>`__) [▶] **⚹ New** → [☾] [🗪] [☉] | | | |
| | [⚒] [☐] [☑] [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#83 (Misc optimizations in Baz) <…>`__ | | | |
| | (by `Rolf Rompen <…>`__ in `Sales team | | | |
| | <…>`__ assigned to `Marc <…>`__) [▶] **🗪 | | | |
| | Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#76 (How to get bar from foo) <…>`__ | | | 34:05 |
| | (by `Romain Raffault <…>`__ in `Managers | | | |
| | <…>`__ assigned to `Luc <…>`__) [▶] **⚒ | | | |
| | Working** → [⚹] [☾] [🗪] [☉] [☐] [☑] [☒] | | | |
| | [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#46 (How can I see where bar?) <…>`__ | | 31:44 | |
| | (by `Romain Raffault <…>`__ in `Managers | | | |
| | <…>`__ assigned to `Romain Raffault | | | |
| | <…>`__) [▶] **⚹ New** → [☾] [🗪] [☉] [⚒] | | | |
| | [☐] [☑] [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#38 (Bar cannot baz) <…>`__ (by `Marc | | | |
| | <…>`__ in `Sales team <…>`__ assigned to | | | |
| | `Rolf Rompen <…>`__) [▶] **🗪 Talk** → | | | |
| | [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| 30 | `#31 (Cannot delete foo) <…>`__ (by `Luc | | 32:22 | |
| | <…>`__ in `Managers <…>`__ assigned to | | | |
| | `Romain Raffault <…>`__) [▶] **⚒ | | | |
| | Working** → [⚹] [☾] [🗪] [☉] [☐] [☑] [☒] | | | |
| | [⧖] | | | |
+--------------------+------------------------------------------+--------------+------------+-----------+
| **Total (7 rows)** | | | **130:13** | **34:05** |
+--------------------+------------------------------------------+--------------+------------+-----------+
Links between tickets¶
A ticket inherits from Hierarchical
, which means that it has a field
parent
, which points to another ticket, which is the “parent” of this
one.
- class lino_noi.lib.tickets.TicketsByParent¶
Shows the tickets that have this ticket as their immediate parent.
>>> obj = tickets.Ticket.objects.get(id=20)
>>> rt.login("robin").show(tickets.TicketsByParent, obj, display_mode="grid")
...
========== ==== ============================= ==================================
Priority ID Summary Assign to
---------- ---- ----------------------------- ----------------------------------
30 21 Irritating message when bar **jean**, **mathieu**, **robin**
========== ==== ============================= ==================================
Filtering tickets¶
Lino Noi modifies the list of the parameters you can use for filtering tickets
by setting a custom params_layout
.
>>> show_fields(tickets.AllTickets, all=True)
...
- Author (user) : The author or reporter of this ticket. The user who reported this
ticket to the database and is responsible for managing it.
- End user (end_user) : Only rows concerning this end user.
- Assigned to (assigned_to) : Only tickets with this user assigned.
- Not assigned to (not_assigned_to) : Only that this user is not assigned to.
- Interesting for (interesting_for) : Only tickets interesting for this partner.
- Subscription (order) : The business document used by both partners as reference for invoicing this ticket.
- State (state) : Only rows having this state.
- Assigned (show_assigned) : Show only (or hide) tickets that are assigned to somebody.
- Active (show_active) : Show only (or hide) tickets which are active (i.e. state is Talk
or ToDo).
- To do (show_todo) : Show only (or hide) tickets that are todo (i.e. state is New
or ToDo).
- Private (show_private) : Show only (or hide) tickets that are marked private.
- Date from (start_date) : Start of observed date range
- until (end_date) : End of observed date range
- Observed event (observed_event) :
- Has reference (has_ref) :
- Commented Last (last_commenter) : Only tickets that have this use commenting last.
- Not Commented Last (not_last_commenter) : Only tickets where this use is not the last commenter.
Change observers¶
A comment is a ChangeNotifier
that forwards its owner’s change
observers:
>>> ar = rt.login('robin')
>>> from lino.modlib.notify.mixins import ChangeNotifier
>>> # Uncomment to find pk of comments having a Group/Company as owner:
>>> # pprint({o.owner:o.pk for o in comments.Comment.objects.order_by('id')})
>>> obj = comments.Comment.objects.get(pk=322)
>>> obj.owner
Group #3 ('Sales team')
>>> isinstance(obj.owner, ChangeNotifier)
True
>>> list(obj.get_change_observers())
...
[(User #4 ('Marc'), <notify.MailModes.often:often>), (User #2 ('Rolf Rompen'),
<notify.MailModes.often:often>)]
>>> list(obj.get_change_observers()) == list(obj.owner.get_change_observers())
True
When the owner of a comment is not a ChangeNotifier, the comment has no change observers:
>>> obj = comments.Comment.objects.get(pk=7)
>>> obj.owner
Company #95 ('Number Three')
>>> isinstance(obj.owner, ChangeNotifier)
False
>>> list(obj.get_change_observers())
[]
>>> list(obj.owner.get_change_observers())
Traceback (most recent call last):
...
AttributeError: 'Company' object has no attribute 'get_change_observers'
>>> list(comments.Comment.objects.get(pk=155).get_change_observers())
...
[]
Don’t read on¶
>>> print(tickets.Ticket.objects.get(pk=45))
#45 (Irritating message when bar)
>>> test_client.force_login(rt.login('robin').user)
>>> def mytest(k):
... url = '/api/tickets/Tickets/{}?dm=list&fmt=json&lv=1697917143.868&mjsts=1695264043.076&mk=0&pv=1&pv&pv&pv&pv&pv&pv&pv&pv&pv&pv&pv=24.10.2023&pv=24.10.2023&pv=10&pv&pv&pv&rp=weak-key-4&ul=en&wt=d'.format(k)
... res = test_client.get(url)
... print(res)
>>> settings.SITE.catch_layout_exceptions = True
>>> mytest("45")
Error during ApiElement.get(): Invalid request for '45' on tickets.Tickets (Unresolved value '10' (<class 'str'>) for tickets.TicketEvents (set Site.strict_choicelist_values to False to ignore this))
Unresolved value '10' (<class 'str'>) for tickets.TicketEvents (set Site.strict_choicelist_values to False to ignore this)
Traceback (most recent call last):
...
lino.core.exceptions.UnresolvedChoice: Unresolved value '10' (<class 'str'>) for tickets.TicketEvents (set Site.strict_choicelist_values to False to ignore this)
Not Found: /api/tickets/Tickets/45
<HttpResponseNotFound status_code=404, "text/html; charset=utf-8">
>>> settings.SITE.catch_layout_exceptions = False
>>> obj = tickets.Ticket.objects.get(pk=115)
>>> obj.state
<tickets.TicketStates.closed:50>
>>> url = f"/api/tickets/ActiveTickets/{obj.pk}?dm=detail&fmt=json"
>>> test_client.force_login(rt.login('robin').user)
>>> res = test_client.get(url)
>>> d = json.loads(res.content.decode())
>>> pprint(d)
{'alert': False,
'message': 'Row 115 is not visible here.<br>But you can see it in <a '
'href="javascript:Lino.tickets.Tickets.detail.run(null,{ '
'"base_params": { }, "param_values": { '
'"assigned_to": null, "assigned_toHidden": '
'null, "end_date": null, "end_user": null, '
'"end_userHidden": null, "has_ref": null, '
'"has_refHidden": null, "interesting_for": '
'null, "interesting_forHidden": null, '
'"last_commenter": null, '
'"last_commenterHidden": null, '
'"not_assigned_to": null, '
'"not_assigned_toHidden": null, '
'"not_last_commenter": null, '
'"not_last_commenterHidden": null, '
'"observed_event": null, '
'"observed_eventHidden": null, "order": null, '
'"orderHidden": null, "show_active": null, '
'"show_activeHidden": null, "show_assigned": '
'null, "show_assignedHidden": null, '
'"show_private": null, "show_privateHidden": '
'null, "show_todo": null, "show_todoHidden": '
'null, "start_date": null, "state": null, '
'"stateHidden": null, "user": null, '
'"userHidden": null }, "record_id": 115 })" '
'style="text-decoration:none">Tickets</a>.',
'success': False,
'title': '<a href="javascript:Lino.tickets.ActiveTickets.grid.run(null,{ '
'"base_params": { }, "param_values": { '
'"assigned_to": null, "assigned_toHidden": null, '
'"end_date": null, "end_user": null, '
'"end_userHidden": null, "has_ref": null, '
'"has_refHidden": null, "interesting_for": null, '
'"interesting_forHidden": null, '
'"last_commenter": null, "last_commenterHidden": '
'null, "not_assigned_to": null, '
'"not_assigned_toHidden": null, '
'"not_last_commenter": null, '
'"not_last_commenterHidden": null, '
'"observed_event": null, "observed_eventHidden": '
'null, "order": null, "orderHidden": null, '
'"show_active": "Yes", '
'"show_activeHidden": "y", '
'"show_assigned": null, "show_assignedHidden": '
'null, "show_private": null, '
'"show_privateHidden": null, "show_todo": null, '
'"show_todoHidden": null, "start_date": null, '
'"state": null, "stateHidden": null, '
'"user": null, "userHidden": null } })" '
'style="text-decoration:none">Active tickets</a>'}