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.

This is a tested document. The following instructions are used for initialization:

>>> 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)
  - (general2):
    - (general2_1): **Subscription** (order), **End user** (end_user), **Ticket type** (ticket_type)
    - (triager_panel_1) [visible for developer admin]: **Project** (site) [visible for all], **Assign to** (quick_assign_to) [visible for all], **Private** (private) [visible for all]
    - (general2_3): **Priority** (priority), **Planned time** (planned_time), **Deadline** (deadline)
    - (general2_4): **Regular** (regular_hours), **Extra** (extra_hours), **Free** (free_hours)
    - **Sessions** (working.SessionsByTicket) [visible for contributor developer admin]
  - (general3): **Workflow** (workflow_buttons), **My comment** (comment), **Comments** (comments.CommentsByRFC)
- **More** (more_tab_1):
  - (more1):
    - (more1_1): **ID** (id), **Reference** (ref)
    - **Summary** (summary)
    - **Description** (description)
  - (more2): **Resolution** (upgrade_notes), **Upload files** (uploads.UploadsByController) [visible for customer contributor developer admin]
  - (more3):
    - **State** (state)
    - **Assigned to** (assigned_to)
    - (more3_3): **Author** (user), **Created** (created)
    - (more3_4): **Modified** (modified), **Fixed since** (fixed_since)
    - **Duplicate of** (duplicate_of)
    - **Duplicates** (DuplicatesByTicket)
- **Links** (links_1):
  - (links_left):
    - **Parent** (parent)
    - **Tickets** (TicketsByParent) [visible for customer contributor developer admin]
  - **Mentioned in** (comments.CommentsByMentioned)
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.

Projects

The list of projects in our demo database depends on who is looking at it. Anonymous users can see only public projects:

>>> rt.show(tickets.Sites)
=========== ============= ======== ================ ======== ============== ====
 Reference   Designation   Client   Contact person   Remark   Workflow       ID
----------- ------------- -------- ---------------- -------- -------------- ----
 admin       admin                                            **⚒ Active**   6
 bugs        bugs                                             **⚒ Active**   3
 cust        cust                                             **⚒ Active**   5
 docs        docs                                             **⚒ Active**   2
 pypi        pypi                                             **⚒ Active**   1
=========== ============= ======== ================ ======== ============== ====
>>> rt.login("marc").show(tickets.Sites)
=========== ============= ======== ================ ======== ================================ ====
 Reference   Designation   Client   Contact person   Remark   Workflow                         ID
----------- ------------- -------- ---------------- -------- -------------------------------- ----
 admin       admin                                            **⚒ Active** → [⚹] [☉] [☾] [☑]   6
 bugs        bugs                                             **⚒ Active** → [⚹] [☉] [☾] [☑]   3
 cust        cust                                             **⚒ Active**                     5
 docs        docs                                             **⚒ Active**                     2
 pypi        pypi                                             **⚒ Active**                     1
=========== ============= ======== ================ ======== ================================ ====

List of sites to which Jean is "subscribed" (i.e. that are assigned to a team where Jean is member):

>>> rt.login("jean").show(tickets.MySites)
======================= ============= ================================
 Project                 Description   Workflow
----------------------- ------------- --------------------------------
 `pypi <Detail>`__                     **⚒ Active** → [⚹] [☉] [☾] [☑]
 `security <Detail>`__                 **⚒ Active** → [⚹] [☉] [☾] [☑]
======================= ============= ================================

List of tickets that have not yet been assigned to a site:

>>> pv = dict(has_site=dd.YesNo.no)
>>> rt.login("robin").show(tickets.AllTickets, param_values=pv)
... 
===== ============================================== ========== ====================================== =========
 ID    Summary                                        Priority   Workflow                               Project
----- ---------------------------------------------- ---------- -------------------------------------- ---------
 116   Foo never bars                                 30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 114   Default account in invoices per partner        30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 112   How can I see where bar?                       30         [✋] [▶] **☒ Refused**
 110   Why is foo so bar                              30         [✋] [▶] **☐ Ready** → [☒]
 108   No more foo when bar is gone                   30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 106   'NoneType' object has no attribute 'isocode'   30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 104   Misc optimizations in Baz                      30         [✋] [▶] **☒ Refused**
 102   Irritating message when bar                    30         [✋] [▶] **☐ Ready** → [☒]
 100   Cannot delete foo                              30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 98    Foo never bars                                 30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 96    Default account in invoices per partner        30         [✋] [▶] **☒ Refused**
 94    How can I see where bar?                       30         [✋] [▶] **☐ Ready** → [☒]
 92    Why is foo so bar                              30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 90    No more foo when bar is gone                   30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 88    'NoneType' object has no attribute 'isocode'   30         [✋] [▶] **☒ Refused**
 86    Misc optimizations in Baz                      30         [✋] [▶] **☐ Ready** → [☒]
 84    Irritating message when bar                    30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 82    Cannot delete foo                              30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 80    Foo never bars                                 30         [✋] [▶] **☒ Refused**
 78    Default account in invoices per partner        30         [✋] [▶] **☐ Ready** → [☒]
 76    How can I see where bar?                       30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 74    Why is foo so bar                              30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 72    No more foo when bar is gone                   30         [✋] [▶] **☒ Refused**
 70    'NoneType' object has no attribute 'isocode'   30         [✋] [▶] **☐ Ready** → [☒]
 68    Misc optimizations in Baz                      30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 66    Irritating message when bar                    30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 64    Cannot delete foo                              30         [✋] [▶] **☒ Refused**
 62    Foo never bars                                 30         [✋] [▶] **☐ Ready** → [☒]
 60    Default account in invoices per partner        30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 58    How can I see where bar?                       30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 56    Why is foo so bar                              30         [✋] [▶] **☒ Refused**
 54    No more foo when bar is gone                   30         [✋] [▶] **☐ Ready** → [☒]
 52    'NoneType' object has no attribute 'isocode'   30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 50    Misc optimizations in Baz                      30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 48    Irritating message when bar                    30         [✋] [▶] **☒ Refused**
 46    Cannot delete foo                              30         [✋] [▶] **☐ Ready** → [☒]
 44    Foo never bars                                 30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 42    Default account in invoices per partner        30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 40    How can I see where bar?                       30         [✋] [▶] **☒ Refused**
 38    Why is foo so bar                              30         [✋] [▶] **☐ Ready** → [☒]
 36    No more foo when bar is gone                   30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 34    'NoneType' object has no attribute 'isocode'   30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 32    Misc optimizations in Baz                      30         [✋] [▶] **☒ Refused**
 30    Irritating message when bar                    30         [✋] [▶] **☐ Ready** → [☒]
 28    Cannot delete foo                              30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 26    Foo never bars                                 30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 24    Default account in invoices per partner        30         [✋] [▶] **☒ Refused**
 22    How can I see where bar?                       30         [✋] [▶] **☐ Ready** → [☒]
 20    Why is foo so bar                              30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 18    No more foo when bar is gone                   30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 16    How to get bar from foo                        30         [✋] [▶] **☒ Refused**
 14    Bar cannot baz                                 30         [✋] [▶] **☐ Ready** → [☒]
 12    Foo cannot bar                                 30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 10    Where can I find a Foo when bazing Bazes?      30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 8     Is there any Bar in Foo?                       30         [✋] [▶] **☒ Refused**
 6     Sell bar in baz                                30         [✋] [▶] **☐ Ready** → [☒]
 4     Foo and bar don't baz                          30         [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 2     Bar is not always baz                          30         [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
===== ============================================== ========== ====================================== =========

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        Extra
 Upgrade       Upgrade            Upgrade            Regular
============= ================== ================== ================

Deciding what to do next

Show all active tickets reported by me.

>>> rt.login('marc').show(tickets.MyTicketsToWork)
... 
========== ====================================== ============ =========
 Priority   Ticket                                 Workflow     Project
---------- -------------------------------------- ------------ ---------
 30         `#83 (Why is foo so bar) <Detail>`__   **☉ Open**   admin
 30         `#35 (Foo never bars) <Detail>`__      **☉ Open**   admin
========== ====================================== ============ =========
>>> rt.login('jean').show(tickets.MyTickets)
... 
========== ================================================================== ============= ============== =============================================
 Priority   Ticket                                                             Assigned to   Planned time   Workflow
---------- ------------------------------------------------------------------ ------------- -------------- ---------------------------------------------
 30         `#113 (Misc optimizations in Baz) <Detail>`__                                                   [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30         `#106 ('NoneType' object has no attribute 'isocode') <Detail>`__                                [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 30         `#99 (No more foo when bar is gone) <Detail>`__                    Luc                          [▶] **☉ Open** → [☾] [☎] [⚒] [☐] [☑] [☒]
 30         `#92 (Why is foo so bar) <Detail>`__                                                            [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 30         `#78 (Default account in invoices per partner) <Detail>`__                                      [✋] [▶] **☐ Ready** → [☒]
 30         `#57 (Irritating message when bar) <Detail>`__                                                  [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30         `#50 (Misc optimizations in Baz) <Detail>`__                                                    [✋] [▶] **☎ Talk** → [☾] [☉] [☐] [☒]
 30         `#43 ('NoneType' object has no attribute 'isocode') <Detail>`__    Robin Rood                   [▶] **☉ Open** → [☾] [☎] [⚒] [☐] [☑] [☒]
 30         `#36 (No more foo when bar is gone) <Detail>`__                                                 [✋] [▶] **⚒ Working** → [☾] [☐] [☒]
 30         `#22 (How can I see where bar?) <Detail>`__                                                     [✋] [▶] **☐ Ready** → [☒]
 30         `#1 (Föö fails to bar when baz) <Detail>`__                                                     [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
========== ================================================================== ============= ============== =============================================

The backlog

The TicketsBySite panel shows all the tickets for a given project. It is a scrum backlog.

>>> pypi = tickets.Site.objects.get(ref="pypi")
>>> rt.login("robin").show(tickets.TicketsBySite, pypi)
... 
==================== ================================================================= ============== ============ ======= ============ =============================================
 Priority             Ticket                                                            Planned time   Regular      Extra   Free         Workflow
-------------------- ----------------------------------------------------------------- -------------- ------------ ------- ------------ ---------------------------------------------
 30                   `#97 ('NoneType' object has no attribute 'isocode') <Detail>`__                  127:03                            [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30                   `#73 (Cannot delete foo) <Detail>`__                                             126:53                            [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30                   `#49 (How can I see where bar?) <Detail>`__                                      127:09                            [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30                   `#25 ('NoneType' object has no attribute 'isocode') <Detail>`__                  130:02               178:42       [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 30                   `#1 (Föö fails to bar when baz) <Detail>`__                                      125:31               168:21       [✋] [▶] **⚹ New** → [☾] [☎] [☉] [⚒] [☐] [☑]
 **Total (5 rows)**                                                                                    **636:38**           **347:03**
==================== ================================================================= ============== ============ ======= ============ =============================================

Anonymous cannot see tickets of a non-public project.

>>> security = tickets.Site.objects.get(ref="security")
>>> rt.show(tickets.TicketsBySite, security)
... 
No data to display

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.
- Project (site) : Show only tickets within this project.
- Has site (has_site) : Show only (or hide) tickets which have a site assigned.
- 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.
- Site Subscriber (subscriber) : Limit tickets to tickets that have a site this user is subscribed to.

Change observers

Don't read this.

>>> ar = rt.login('robin')
>>> list(tickets.Site.objects.get(ref="docs").get_change_observers())
... 
[(User #6 ('Luc'), <notify.MailModes.often:often>), (User #3 ('Romain
Raffault'), <notify.MailModes.often:often>)]
>>> list(comments.Comment.objects.get(pk=1).get_change_observers())
... 
[(User #7 ('Jean'), <notify.MailModes.often:often>), (User #5 ('Mathieu'),
<notify.MailModes.often:often>), (User #1 ('Robin Rood'),
<notify.MailModes.often:often>)]
>>> list(comments.Comment.objects.get(pk=155).get_change_observers())
... 
[(User #4 ('Marc'), <notify.MailModes.often:often>), (User #2 ('Rolf Rompen'),
<notify.MailModes.often:often>)]