Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

tickets (Ticket management 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.

Side note: Code snippets (lines starting with >>>) in this document get tested as part of our development workflow. The following initialization snippet tells you which demo project is being used in this document.

>>> from lino import startup
>>> startup('lino_book.projects.noi1e.settings.demo')
>>> from lino.api.doctest 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** (DuplicatesByTicket)
class lino_noi.lib.tickets.Ticket

The Django model used to represent a ticket in Noi. Adds some fields and methods.

assigned_to

The user who is working on this ticket.

site

The site this ticket belongs to. You can select only sites you are subscribed to.

Screenshots

../../_images/tickets.Ticket.merge.png

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 line   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 no permission to modify the workflow state of tickets owned by other users. But he can have tickets to do, i.e. that are assiged to him.

>>> rt.login('marc').show(tickets.MyTicketsToWork, max_width=40, display_mode="grid")
... 
+----------+------------------------------------------+---------------+
| Priority | Ticket                                   | Workflow      |
+==========+==========================================+===============+
| 30       | `#103 (Cannot delete foo) <…>`__ (by     | **⚒ Working** |
|          | `Romain Raffault <…>`__ in `Developers   |               |
|          | <…>`__ assigned to `Marc <…>`__)         |               |
+----------+------------------------------------------+---------------+
| 30       | `#85 ('NoneType' object has no attribute | **⚒ Working** |
|          | 'isocode') <…>`__ (by `Jean <…>`__ in    |               |
|          | `Developers <…>`__ assigned to `Marc     |               |
|          | <…>`__)                                  |               |
+----------+------------------------------------------+---------------+
| 30       | `#67 (Cannot delete foo) <…>`__ (by      | **⚒ Working** |
|          | `Mathieu <…>`__ in `Developers <…>`__    |               |
|          | assigned to `Marc <…>`__)                |               |
+----------+------------------------------------------+---------------+
| 30       | `#55 (Cannot delete foo) <…>`__ (by      | **⚹ New**     |
|          | `Rolf Rompen <…>`__ in `Developers       |               |
|          | <…>`__ assigned to `Marc <…>`__)         |               |
+----------+------------------------------------------+---------------+
| 30       | `#37 ('NoneType' object has no attribute | **⚹ New**     |
|          | 'isocode') <…>`__ (by `Luc <…>`__ in     |               |
|          | `Developers <…>`__ assigned to `Marc     |               |
|          | <…>`__)                                  |               |
+----------+------------------------------------------+---------------+
| 30       | `#19 (Cannot delete foo) <…>`__ (by      | **⚹ New**     |
|          | `Romain Raffault <…>`__ in `Developers   |               |
|          | <…>`__ assigned to `Marc <…>`__)         |               |
+----------+------------------------------------------+---------------+
>>> rt.login('jean').show(tickets.MyTickets, max_width=30, display_mode="grid")
... 
+----------+--------------------------------+-----------------+--------------------------------+
| Priority | Ticket                         | Assigned to     | Workflow                       |
+==========+================================+=================+================================+
| 30       | `#92 (Why is foo so bar)       |                 | [▶] **☎ Talk** → [⚹] [☾] [☉]   |
|          | <…>`__ [▶] **☎ Talk** → [⚹]    |                 | [☐] [☑] [☒] [⧖]                |
|          | [☾] [☉] [☐] [☑] [☒] [⧖]        |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#85 ('NoneType' object has no | Marc            | [▶] **⚒ Working** → [⚹] [☾]    |
|          | attribute 'isocode') <…>`__    |                 | [☎] [☉] [☐] [☑] [☒] [⧖]        |
|          | (in `Developers <…>`__         |                 |                                |
|          | assigned to `Marc <…>`__) [▶]  |                 |                                |
|          | **⚒ Working** → [⚹] [☾] [☎]    |                 |                                |
|          | [☉] [☐] [☑] [☒] [⧖]            |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#78 (No more foo when bar is  |                 | [▶] **☐ Ready** → [⚹] [☾] [☎]  |
|          | gone) <…>`__ [▶] **☐ Ready** → |                 | [☑] [☒]                        |
|          | [⚹] [☾] [☎] [☑] [☒]            |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#64 (How to get bar from foo) |                 | [▶] **⚹ New** → [☾] [☎] [☉]    |
|          | <…>`__ [▶] **⚹ New** → [☾] [☎] |                 | [☐] [☑] [⧖]                    |
|          | [☉] [☐] [☑] [⧖]                |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#57 (Irritating message when  |                 | [■] **☉ Open** → [⚹] [☾] [☎]   |
|          | bar) <…>`__ (in `Managers      |                 | [⚒] [☐] [☑] [☒] [⧖]            |
|          | <…>`__) [■] **☉ Open** → [⚹]   |                 |                                |
|          | [☾] [☎] [⚒] [☐] [☑] [☒] [⧖]    |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#29 (Foo never bars) <…>`__   | Romain Raffault | [▶] **☎ Talk** → [⚹] [☾] [☉]   |
|          | (in `Sales team <…>`__         |                 | [⚒] [☐] [☑] [☒] [⧖]            |
|          | assigned to `Romain Raffault   |                 |                                |
|          | <…>`__) [▶] **☎ Talk** → [⚹]   |                 |                                |
|          | [☾] [☉] [⚒] [☐] [☑] [☒] [⧖]    |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#22 (How can I see where      |                 | [▶] **⚒ Working** → [⚹] [☾]    |
|          | bar?) <…>`__ [▶] **⚒ Working** |                 | [☎] [☉] [☐] [☑] [☒] [⧖]        |
|          | → [⚹] [☾] [☎] [☉] [☐] [☑] [☒]  |                 |                                |
|          | [⧖]                            |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#15 (Bars have no foo) <…>`__ | Mathieu         | [▶] **☐ Ready** → [⚹] [☾] [☎]  |
|          | (in `Managers <…>`__ assigned  |                 | [☑] [☒]                        |
|          | to `Mathieu <…>`__) [▶] **☐    |                 |                                |
|          | Ready** → [⚹] [☾] [☎] [☑] [☒]  |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+
| 30       | `#1 (Föö fails to bar when     |                 | [▶] **⚹ New** → [☾] [☎] [☉]    |
|          | baz) <…>`__ (in `Developers    |                 | [⚒] [☐] [☑] [⧖]                |
|          | <…>`__) [▶] **⚹ New** → [☾]    |                 |                                |
|          | [☎] [☉] [⚒] [☐] [☑] [⧖]        |                 |                                |
+----------+--------------------------------+-----------------+--------------------------------+

The backlog

The TicketsBySite panel shows all the tickets for a given project. 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 |
+====================+==========================================+==============+===========+======+
| 30                 | `#91 (Cannot delete foo) <…>`__ (by      |              |           |      |
|                    | `Robin Rood <…>`__ in `Developers <…>`__ |              |           |      |
|                    | assigned to `Rolf Rompen <…>`__) [▶] **⚹ |              |           |      |
|                    | New** → [☾] [☎] [☉] [⚒] [☐] [☑] [⧖]      |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#83 (Misc optimizations in Baz) <…>`__  |              | 65:18     |      |
|                    | (by `Rolf Rompen <…>`__ in `Sales team   |              |           |      |
|                    | <…>`__ assigned to `Luc <…>`__) [▶] **☎  |              |           |      |
|                    | Talk** → [⚹] [☾] [☉] [⚒] [☐] [☑] [☒] [⧖] |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#76 (How to get bar from foo) <…>`__    |              |           |      |
|                    | (by `Rolf Rompen <…>`__) [▶] **⚒         |              |           |      |
|                    | Working** → [⚹] [☾] [☎] [☉] [☐] [☑] [☒]  |              |           |      |
|                    | [⧖]                                      |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#46 (How can I see where bar?) <…>`__   |              |           |      |
|                    | (by `Mathieu <…>`__) [▶] **⚹ New** → [☾] |              |           |      |
|                    | [☎] [☉] [☐] [☑] [⧖]                      |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#38 (Bar cannot baz) <…>`__ (by `Marc   |              |           |      |
|                    | <…>`__) [▶] **☎ Talk** → [⚹] [☾] [☉] [☐] |              |           |      |
|                    | [☑] [☒] [⧖]                              |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#31 (Cannot delete foo) <…>`__ (by      |              |           |      |
|                    | `Marc <…>`__ in `Developers <…>`__       |              |           |      |
|                    | assigned to `Rolf Rompen <…>`__) [▶] **⚒ |              |           |      |
|                    | Working** → [⚹] [☾] [☎] [☉] [☐] [☑] [☒]  |              |           |      |
|                    | [⧖]                                      |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| 30                 | `#1 (Föö fails to bar when baz) <…>`__   |              |           |      |
|                    | (by `Jean <…>`__ in `Developers <…>`__)  |              |           |      |
|                    | [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]  |              |           |      |
|                    | [⧖]                                      |              |           |      |
+--------------------+------------------------------------------+--------------+-----------+------+
| **Total (7 rows)** |                                          |              | **65:18** |      |
+--------------------+------------------------------------------+--------------+-----------+------+

Filtering tickets

Lino Noi modifies the list of the parameters you can use for filterings tickets be 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
>>> obj = comments.Comment.objects.filter(group__isnull=False).first()
>>> obj.owner
Group #3 ('Sales team')
>>> isinstance(obj.owner, ChangeNotifier)
True
>>> list(obj.get_change_observers())
... 
[(User #6 ('Luc'), <notify.MailModes.often:often>), (User #3 ('Romain
Raffault'), <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.filter(group__isnull=True).first()
>>> obj.owner
Company #181 ('Saffre-Rumma')
>>> 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&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)
>>> mytest("45")  
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)