Skip to content

Latest commit

 

History

History
1633 lines (1273 loc) · 61.9 KB

pm-task.org

File metadata and controls

1633 lines (1273 loc) · 61.9 KB

pm-task.org: Tasks

This library extends Org mode’s to-do items with more robust tools for external reporting, especially on multiuser tasks. Org already supports the following:

  • Task status
  • Event logging via the logbook, including automation-assist for logging status changes
  • Deadlines
  • A reasonably powerful query engine geared for use with the Org agenda

The main extensions this library provides are:

  • Additional metadata, such as assignee
  • A different query engine geared toward collecting status information for reporting

Acquiring tasks is not the responsbility of this library; tasks are just headlines with to-do keywords, Org has several ways to acquire those. It will provide some tools for filtering lists of tasks once they’re acquired.

Project Management Versus Task Tracking

Org implements a to-do tracker for an individual; this package implements tasks for project management, possibly among many users. With different goals come differences in nomenclature. Moreover, Org defines most of the elements of its to-do system syntactically, although some semantics are implied in the support tools Org implements, like clocks or the Org agenda. Put another way, Org gives you keywords and tags and logbook entries. Org mostly avoids defining what that means.

By contrast, this package specifically imposes a semantic framework on these structures, drawn from that of project management and “issue tracking” tools such as Bugzilla and Jira. This model consists of the following elements:

  • Tasks (sometimes issues) reflect finite-time, finite-effort units of work. Tasks may be typed.
  • The work in tasks may be done by many people. (Or, rarely, other entities like automated processes or teams.) One person is primarily responsible for getting the work done at a given time; that person is said to be assigned the task.
  • A task has a lifecycle; it transitions between different states or statuses before a final disposition. Other events of interest may also occur in that time. (Those events’ semantics often less well-defined, and may degenerate all the way back to a syntactic definition “Freeform text associated with a timestamp.”)
  • Tasks usually have a textual description. The description is associated with the event of task creation, though it may be modified afterward.
  • While these are the core features of task trackers oriented toward project management, most associate additional metadata, such as related tasks or associated files. Allowable metadata may be defined by task type or status. (For instance, an issue that has been resolved may carry metadata about how it was resolved.)

That said, this package is focused only on the construction of a rich task construct, which can be used in many ways by a project manager. To that end, every extension this package offers is optional; the most minimal task is a headline with a to-do item. In particular, this system should be easy to overlay onto preexisting Org files that have not been designed with this system in mind and still provide some value.

Future Work

Expanded comments

Annotations provide a mechanism for single-paragraph comments, and are mainly intended for providing timestamped updates on the completion of a task. Descriptive material longer than a single paragraph can be put in the description.

There are one or two compelling arguments for a more expressive comment mechanism, mainly in the event that this system gets multiple users. In such an environment, the task description is a more privileged space than it is with a single user, so a space for richer per-user elaboration would be welcome. Headlines can also accommodate metadata more easily, which would be needed to support, say, threaded conversations. Finally, this sort of comment system may be necessary if interoperability with other issue trackers such as Jira were ever required.

A possible format for such a change is in Listing ex/future/comments/1. Note that each comment is both dated and has a corresponding logbook entry, also dated. Without this denormalizing, it would be harder to compute recent changes, or access date information from the comment when it is accessed as an Org element. The duplication adds to the size of the task, but shouldn’t require updating, since log entries should never be modified.

At any rate, this is all notional for now.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :CUSTOM_ID: task-12345
  :END:
  :LOGBOOK:
  - New comment [[#comment-9876]] from "Vince Lombardi" on [2021-12-10 Fri 09:10]
  - New comment [[#comment-9875]] from "Tobin Rote" on [2021-12-10 Fri 09:05]
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:36] \\
    Back on the case
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:25] \\
    Waiting on parts from the supplier
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:14] \\
    In process, it's harder than it looks
  - Note taken on [2021-12-10 Fri 08:06] \\
    Task created
  :END:
  :CHANGES:
  - [2021-12-10 Fri 09:10] \\

  - [2021-12-10 Fri 08:04] \\
    Assignment changed to "Bart Starr" from ""
  :END:

** Comments

*** Bart just won Super Bowl I
    :PROPERTIES:
    :CUSTOM_ID: comment-9876
    :CREATED:  [2021-12-10 Fri 09:10]
    :CREATOR: Vince Lombardi
    :IN_REPLY_TO: comment-9875
    :END:

    He'll be back in the office in a few weeks.

*** What's the status?
    :PROPERTIES:
    :CUSTOM_ID: comment-9875
    :CREATED:  [2021-12-10 Fri 09:05]
    :CREATOR: Tobin Rote
    :END:

    A customer asked about the security system. Just checking....

Code

Package Header

;;; pm-task.el --- Task handling for project management

;; Copyright (C) 2021 Phil Groce

;; Author: Phil Groce <pgroce@gmail.com>
;; Version: 0.1.6

;; Package-Requires: ((emacs "26.1") (dash "2.19") (s "1.12") (org-ml "5.7") (ts "0.3") (pg-ert "0.1") (pg-org "0.4"))
;; Keywords: productivity

Requires

(require 'dash)
(require 's)
(require 'ts)
(require 'org-ml)
(require 'pg-ert)
(require 'pg-org)

Tasks

The simplest possible task is contained in Listing ex/task/1.

* DOING Rewire the security system

As mentioned in #sct-pm-vs-todo, a Task is just an Org to-do item, and the most minimal to-do item is, syntactically, a Task. Where possible, pm-task keeps the syntax and semantics of existing Org to-do mechanism like the to-do tags themselves, the LOGBOOK drawer, etc. A few additional semantic elements are also defined, built on existing Org primitives as much as possible.

Listing ex/task/2 shows a simple Org file containing a single task. This task also contains a logbook with several entries. One feature shown here that is not common to Org to-do items is an Assignee. This doesn’t make much sense for Org’s single-user task tracking, but the Tasks defined here are for managing projects with multiple contributors. The parts of a Task shown here are listed below; the description of a Task uses Org’s structural terminology, refer to the Org manual, particularly the manual for the org-element package, if terms are unfamiliar.

  • The status, shown as the to-do keyword. Here, that status corresponds directly to the most recent logbook entry, meaning the Task is synchronized. This package can work with Tasks that are not synchronized, but not with full functionality.
  • The task title is the title of the headline.
  • The logbook entries tracking changes to the to-do keyword are referred to here as status changes.
#+seq_todo: TODO DOING(@) BLOCKED(@) | DONE(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :END:
  :LOGBOOK:
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:06] \\
    Back on the case
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:05] \\
    Waiting on parts from the supplier
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:04] \\
    In process, it's harder than it looks
  :END:

  This is a good place to describe the issue. You can currently put anything you like here.

** Subheadings

   You can also include subheadings and [[https://google.com/][links]] and anything else that makes sense.

Everything in Listing ex/task/2 is standard Org syntax used in ways that Org expects. Extensions to this model have their own semantics, but are still constructed from Org primitives. A task exhibiting all the features pm-task supports is contained Listing ex/task/3. It demonstrates the following additional features:

  • An assignee, a user who is responsible for finishing the task. The default assignee is “UNASSIGNED” (Canonically uppercase, but case-insensitive variants will be accepted. The package doesn’t support any version of “unassigned” as a valid username.)
  • Unique IDs for the task and for each comment.
  • Additional logbook entries tracking event creation and comment editing

Comments and the non-status events in the logbook are currently aspirational, see #sct-future-work.

Building a task

The code in Listing src/build generates a task from input parameters, as a data structure built from Org elements. The word build will be used to refer to this kind of creation, as opposed to hand-building a task in a buffer, or deriving a text representation from an Org element (likely also for insertion into a buffer).

(defun pm-task-build (the-headline keyword description assignee user)
  (let* ((now-ts  (ts-now))
         (now-str (ts-format "[%Y-%m-%d %a %H:%M]" now-ts))
         (now-org `(timestamp 'inactive
                              ,(ts-year now-ts)
                              ,(ts-month now-ts)
                              ,(ts-day now-ts)
                              ,(ts-year now-ts)
                              ,(ts-month now-ts)
                              ,(ts-day now-ts)
                              :hour-start ,(ts-hour now-ts)
                              :minute-start ,(ts-minute now-ts)
                              :hour-end ,(ts-hour now-ts)
                              :minute-end ,(ts-minute now-ts)))
         (user (or user (user-login-name)))
         ;; no default for assignee; if it's nil, don't include it
         (the-headline (or the-headline ""))
         (keyword (or keyword "TODO"))
         (description (or description ""))
         (assignee (or assignee "")))
    `(headline
      :title (secondary-string! ,the-headline)
      :todo-keyword ,keyword
      (section
       (property-drawer
        ;; Interestingly, org-element stores node properties as strings,
        ;; including org-mode timestamps. But in a buffer, they're
        ;; handled as regular timestamps. ¯\_(ツ)_/¯
        (node-property "CREATED" ,now-str)
        (node-property "CREATOR" ,user)
        (node-property "ASSIGNEE" ,assignee)
        (node-property "CUSTOM_ID" ,(format "pm-task-%s" (org-id-uuid))))
       (drawer
        "LOGBOOK"
        :post-blank 1
        (plain-list
         (item (paragraph
                ,(format "Task created by \"%s\" on %s" user now-str)))))
       (paragraph! ,description)))))

The pm-task-build function can be exercised with the code in Listing ex/build/1, with the result in Listing ex/build/1/result.

<<src/build>>
(->> (pm-task-build "FooBar" "TODO" "Foo the bar!" "Harry Bovik" "Frank Gorshin")
     (pg-org-ml-build)
     (org-ml-to-trimmed-string))
* TODO FooBar
:PROPERTIES:
:CREATED:  [2022-01-24 Mon 19:11]
:CREATOR:  Frank Gorshin
:ASSIGNEE: Harry Bovik
:CUSTOM_ID: pm-task-17c6ffbc-1ab9-44c1-b48a-b973b7aeed43
:END:
:LOGBOOK:
- Task created by "Frank Gorshin" on [2022-01-24 Mon 19:11]
:END:

Foo the bar!

Accessing and updating parts of the task data structure

These functions access and update parts of a task. They operate on and return tasks as Org element data structures.

These functions are obviously useful for programmatic manipulation of tasks, and users may always, if they wish, modify the text representations of tasks in any way they see fit. However, these functions also perform additional housekeeping on tasks, such as change logging and (possibly, in the future) adding appropriate cross-references. Several other functions in this package require this metadata to work (as noted in their documentation), so users may find it advantageous to use these functions when possible.

Below are low-level functions for manipulating the logbook, followed by higher-level functions for updating various parts of a task. In general, the high-level interface tries to log everything it does, but since the task also exists as text in an Org file that a user can change arbitrarily, these should be seen as a convenience, and not any kind of guarantee that the log is complete or authoritative.

If you need change control, check project files containing tasks into version control systems. These changes will not be synced with individual changes made to a task, but neither is this package ever likely to try to replicate that functionality. (Automatically checking a project into version control when a task changes is a possible feature, but unplanned.)

The task update functions all take tasks represented as Org elements and return new tasks as Org elements. All changes are noted in the updated task’s logbook, unless the changes themselves are updates to the logbook, such as new notes.

A final note: These functions generate tasks with many optional features. Minimally, this usually menas logs of actions taken in the logbook. Additional work may include adding UUIDs and creating cross-references.

Working with the logbook

The following functions manage the logbook, where the task creation and update functions log changes. Users can also add annotations to the logbook with the pm-task-annotate function in Section #sct-annotate.

Getting and setting the logbook

We get the logbook using the code in Listing src/logbook/get. Org ML provides some support for this in org-ml-headline-get-logbook-items and org-ml-headline-set-logbook-items, but for the interface of this package it would be more convenient to be able to get/set the whole logbook drawer.

(defun pm-task-get-logbook (task)
  "If TASK has a logbook, returns a copy of it as an org-element
expression. If not, returns a new, empty logbook."
  (let ((logbook-name (org-log-into-drawer)))
    (-if-let (logbook (org-ml-match
                       `(section (:and drawer (:drawer-name ,logbook-name)))
                       task))
        (nth 0 logbook)
      (org-ml-build-drawer logbook-name))))

A task can then be updated with a new logbook using the pm-task-set-logbook function in Listing src/logbook/set. This can be used to completely replace a task’s logbook, as shown in Listing ex/logbook/set/1/code and Listing ex/logbook/set/1/results for the task in Listing ex/logbook/set/1/input. Replacing the logbook destroys any history present in the logbook, and should only be used for low-level manipulations. The task update functions are the end-user interface to making programmatic task changes. Listing ex/logbook/set/2/code uses pm-task-set-logbook in conjunction with pg-org-logbook-prepend-item to note task changes; the results of this change are in ex/logbook/set/2/results.

(defun pm-task-set-logbook (new-logbook task)
  "Make NEW-LOGBOOK the logbook in TASK. If a logbook already
  exists in TASK, it is replaced with NEW-LOGBOOK."
  (let ((matcher
         '(:first section
                  (:and drawer
                        (:drawer-name "LOGBOOK")))))
    (if (org-ml-match matcher task)
        ;; Replace old logbook with new one
        (org-ml-match-replace matcher new-logbook task)
      ;; Insert new logbook after property drawer, if it exists. If
      ;; not, insert it as the first child.
      (let* ((section (org-ml-headline-get-section task))
             (sct-children (org-ml-get-children section))
             (pdrawer-idx (--find-index
                           (org-ml-is-type 'property-drawer)
                           sct-children))
             (new-children
              (if (numberp pdrawer-idx)
                  (-insert-at (+ 1 pdrawer-idx) new-logbook sct-children)
                (-insert-at 0 new-logbook sct-children))))
        (org-ml-headline-set-section new-children)))))

(defun pm-task-set-logbook-2 (task new-logbook)
  "Equivalent to `pm-task-set-logbook', but with arguments
  reversed for easier function composition."
  (pm-task-set-logbook new-logbook task))
Testing: pm-task-get-logbook and pm-task-set-logbook
#+seq_todo: TODO DOING(@) BLOCKED(@) | DONE(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :END:
  :LOGBOOK:
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:06] \\
    Third item added
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:05] \\
    Second item added
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:04] \\
    First item added
  :END:

This is the description.
<<src/logbook/get>>
<<src/logbook/set>>

(pg-org-with-src-doc ex/logbook/set/1/input

  (let* ((lb-item
          (org-ml-build-log-note
           (ts-unix (ts-now))
           "New item"))
         (new-logbook
          (->> (pg-org-logbook t)
               (pg-org-logbook-prepend-item lb-item))))
    (cl-assert new-logbook)
    (->> doc
         (org-ml-match '(:first headline))
         (nth 0)
         (pm-task-set-logbook new-logbook)
         (org-ml-to-trimmed-string))))
* DOING Rewire the security system
:PROPERTIES:
:ASSIGNEE: Bart Starr
:END:
:LOGBOOK:
- Note taken on [2022-01-21 Fri 07:14] \\
  New item
:END:
This is the description.
<<src/logbook/get>>
<<src/logbook/set>>

(pg-org-with-src-doc ex/logbook/set/1/input

  (let* ((task (->> doc
                    (org-ml-match '(:first headline))
                    (nth 0)))
         (lb-item
          (org-ml-build-log-note
           (ts-unix (ts-now)) "New item"))
         (new-logbook
          (->> (pm-task-get-logbook task)
               (pg-org-logbook-prepend-item lb-item))))
    (cl-assert new-logbook)
    (->> (pm-task-set-logbook new-logbook task)
         (org-ml-to-trimmed-string))))
* DOING Rewire the security system
:PROPERTIES:
:ASSIGNEE: Bart Starr
:END:
:LOGBOOK:
- Note taken on [2022-01-21 Fri 10:47] \\
  New item
- State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:06] \\
  Third item added
- State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:05] \\
  Second item added
- State "DOING"      from "TODO"       [2021-12-11 Sat 20:04] \\
  First item added
:END:
This is the description.

Adding a log entry

We can combine these techniques into a general-purpose logging function, as seen in Listing src/logbook/log. Example ex/logbook/log shows an example of use, generating the output in ex/logbook/log/result.

(defun pm-task-log (header msg task)
  "Create a new task based on TASK, with a new log entry in the
logbook. The log entry will contain a header as specified by
HEADER, a space, a timestamp, a line break, and the contents of
MSG, a string."
  (let* ((now (->> (ts-now)
                   (pg-org-build-timestamp-from-ts nil)))
         (header (or header (format
                             "Log entry by \"%s\""
                             user-login-name)))
         (break (org-ml-build-line-break))
         (new-entry (if msg
                        (org-ml-build-paragraph
                         header " " now  " " break msg)
                      (org-ml-build-paragraph
                       header " " now))))
    (--> (pm-task-get-logbook task)
         (pg-org-logbook-prepend-paragraph new-entry it)
         (pm-task-set-logbook it task))))
Testing: pm-task-log
(pg-org-with-src-doc ex/logbook/set/1/input

  (->> doc
       (org-ml-match '(:first headline))
       (nth 0)
       (pm-task-log "Header test" "This is a message")
       (pm-task-log "nother test" nil)
       (pm-task-log nil "Saying something in the msg")
       (pm-task-log nil nil)
       (org-ml-to-trimmed-string)))

Adding an annotation

The pm-task-annotate function in Listing src/logbook/annotate uses the techniques demonstrated in Section sct-logbook to add an annotation to the task. (Org already has something called a note, and comment is a better name for a planned feature, leaving annotation as the best available term for this concept.)

;; This method of interpolation borrowed from org-ml--log-replace-*
(defun pm-task-annotate (msg task)
  "Annotate TASK with MSG. User and time are also logged in this
message. User is taken from `user-login-name'; if this is not
set, the empty string will be used."
  (pm-task-log
   (format "Annotation by \"%s\" on" (or user-login-name "")) msg task))
Testing: pm-task-annotate

Use of pm-task-annotate is shown in Listing ex/logbook/1, with results in ex/logbook/1/results. (Assuming the logged-in user is asmithee.)

<<src/logbook/annotate>>

(pg-org-with-src-doc ex/logbook/set/1/input
  (->> doc
       (org-ml-match '(:first headline))
       (nth 0)
       (pm-task-annotate "Just a test")
       (org-ml-to-trimmed-string)))
* DOING Rewire the security system
:PROPERTIES:
:ASSIGNEE: Bart Starr
:END:
:LOGBOOK:
- Annotation by "asmithee" on [2022-01-27 Thu 20:01] \\
  Just a test
- State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:06] \\
  Third item added
- State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:05] \\
  Second item added
- State "DOING"      from "TODO"       [2021-12-11 Sat 20:04] \\
  First item added
:END:
This is the description.

Assigning/Reassigning a task

The pm-task-reassign function changes the assignee. The function is defined in Listing src/reassign, and used in Example ex/reassign/1 on the task in Example ex/logbook/1/set/input, with results in Example ex/reassign/1/results.

(defun pm-task-reassign (new-assignee task)
  (let* ((old-assignee (or (pm-task-assignee task) ""))
         (header
          (format
           "User \"%s\" changed assignee from \"%s\" to \"%s\""
           user-login-name old-assignee new-assignee)))
    (->> task
         (pg-org-headline-set-node-property
          nil 'replace
          "ASSIGNEE" (list new-assignee))
         (pm-task-log header nil))))

Testing: pm-task-reassign

<<src/reassign>>

(pg-org-with-src-doc ex/logbook/set/1/input
  (->> doc
       (org-ml-match '(:first headline))
       (nth 0)
       (pm-task-reassign "Bret Farvrvre")
       (org-ml-to-trimmed-string)))
* DOING Rewire the security system
:PROPERTIES:
:ASSIGNEE: Bret Farvrvre
:END:
:LOGBOOK:
- User "pgroce" changed assignee from "Bart Starr" to "Bret Farvrvre" [2022-02-02 Wed 20:23]
- State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:06] \\
  Third item added
- State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:05] \\
  Second item added
- State "DOING"      from "TODO"       [2021-12-11 Sat 20:04] \\
  First item added
:END:
This is the description.

Getting parts of a logbook item

pm-task-logbook-item-get-parts

This may be moved to pg-org eventually, but for now, these functions break logbook items into component parts and return them.

(defun pm-task--logbook-item-get-parts (item)
  "Return the different parts of ITEM.

Returns a three-element list of the message header (as a
paragraph object), the timestamp (as a timestamp object), and the
message (as a paragraph object)"
  (cl-letf (((symbol-function 'is-long-inactive-timestamp)
             (lambda (node)
               (when (and (org-ml-is-type 'timestamp node)
                          (org-ml--property-is-eq :type 'inactive node)
                          (-some->> (org-ml--timestamp-get-start-time node)
                            (org-ml-time-is-long)))
                 (org-ml--timestamp-get-start-unixtime node))))

            ((symbol-function 'is-line-break)
             (lambda (node)
               (or (org-ml-is-type 'line-break node)
                   (and (org-ml-is-type 'plain-text node)
                        (equal "\n" node)))))

            ((symbol-function 'get-paragraph-children)
             (lambda (item)
               (-when-let (first-child (car (org-ml-get-children item)))
                 (when (org-ml-is-type 'paragraph first-child)
                   (org-ml-get-children first-child))))))

    ;; Split the children on the line break, if it exists
    (-let* (((left right) (->> item
                               (get-paragraph-children)
                               (-split-when #'is-line-break)
                               ))
            (timestamp (when (is-long-inactive-timestamp (-last-item left))
                         (-last-item left)))
            (left-parts (if timestamp
                            (-slice left 0 -1)
                          left))
            (left-para (apply #'org-ml-build-paragraph left-parts))
            (right-para (apply #'org-ml-build-paragraph right)))
      (list left-para timestamp right-para))))

(defun pm-task-logbook-item-get-header (item)
  "Return the header associated with the logbook item ITEM."
  (nth 0 (pm-task--logbook-item-get-parts item)))

(defun pm-task-logbook-item-get-timestamp (item)
  "Return the timestamp associated with the logbook item ITEM."
  (nth 1 (pm-task--logbook-item-get-parts item)))

(defun pm-task-logbook-item-get-message (item)
  "Return the message associated with the logbook item ITEM."
  (nth 2 (pm-task--logbook-item-get-parts item)))

Testing: pm-task-logbook-item-get-parts

<<src/logbook/item-get-parts>>


(ert-deftest pm-task/item-get-parts ()
  (let ((item (org-ml-from-string 'item "- Note from user \"pgroce\" at [2021-12-11 Sat 20:04] \\\\\n A test ")))
    (should (equal
             (->> item
                  (pm-task--logbook-item-get-parts)
                  (-map #'org-ml-to-trimmed-string))
             '("Note from user \"pgroce\" at"
               "[2021-12-11 Sat 20:04]"
               "A test")))
    (should (equal
             (->> item
                  (pm-task-logbook-item-get-header)
                  (org-ml-to-trimmed-string))
             "Note from user \"pgroce\" at"))
    (should (equal
             (->> item
                  (pm-task-logbook-item-get-timestamp)
                  (org-ml-to-trimmed-string))
             "[2021-12-11 Sat 20:04]"))
    (should (equal
             (->> item
                  (pm-task-logbook-item-get-message)
                  (org-ml-to-trimmed-string))
             "A test"))))

(pg-ert-run-tests-string "pm-task/item-get-parts")

Not implemented

The following update functions were considered and rejected.

Task title
This is easily done programmatically with org-ml-headline-set-title!.
Task description
Programmatically, this can be done with org-ml-headline-set-section and, perhaps, adding additional headlines with org-ml-set-children. More fundamentally, this makes much more sense to do simply by editing the text.

These design decisions may be revisited in the future, probably only in order to do logging.

Accessing task information

Input for tests in this section is in Listing input/basic.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :CREATED:  [2021-12-01 Wed 10:00]
  :CREATOR: Vince Lombardi
  :END:
  :LOGBOOK:
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:36] \\
    Back on the case
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:25] \\
    Waiting on parts from the supplier
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:14] \\
    In process, it's harder than it looks
  :END:

* DONE Get past the guard
  :LOGBOOK:
  - State "DONE"       from "TODO"       [2021-12-31 Fri 17:48] \\
    Got past 'em, easy peasy
  :END:

Status changes

The code in Listing src/accessors extracts information from tasks represented as Org element trees.

(defun pm-task-status-changes (task)
  "Returns all status change logbook entries for TASK as a list
    of records `(from to when notes)', where `from' and `to' are
    the original and changed statuses, `when' is the org-element
    representation of the timestamp, and `notes' is an Org
    secondary string containing any associated notes."
  (->> (pg-org-headline-logbook-entries task)
       (-keep #'pg-org-paragraph-parse-status-change)))

(defun pm-task-status-last-change (task)
  "Returns the most recent status change logbook entry for
    TASK. For the format of this record see
    `pm-task-status-changes'"
  (nth 0 (pm-task-status-changes task)))

(defun pm-task-status-last-change-from (task)
  "Returns the status changed from in the most recent logbook
  entry for TASK."
  (nth 0 (pm-task-status-last-change task)))

(defun pm-task-status-last-change-to (task)
  "Returns the status changed to in the most recent logbook entry
  for TASK."
  (nth 1 (pm-task-status-last-change task)))

(defun pm-task-status-last-change-timestamp (task)
  "Returns the timestamp in the most recent logbook entry for
  TASK, as a ts.el timestamp object."
  (->> (nth 2 (pm-task-status-last-change task))
       (ts-parse-org-element)))

(defun pm-task-status-last-change-org-timestamp (task)
  "Returns the timestamp in the most recent logbook entry for
  TASK, as an org-element."
  (nth 2 (pm-task-status-last-change task)))

(defun pm-task-status-last-change-notes (task)
  "Returns the notes in the most recent logbook entry for
  TASK."
  (nth 3 (pm-task-status-last-change task)))

  ;; I'm making a Big Assumption that entries in the logbook will
  ;; already be sorted by time.

(defun pm-task-current-status (task)
  "Returns the current status of TASK. The current status is the
    todo keyword of the headline; if the headline has no todo
    keyword, it is `nil'.

  This may not be synchronized with the most recent logbook entry;
  if the user wants this, they must call ()`pm-task-is-synced'
  first."
  (org-ml-get-property :todo-keyword task))

Testing: pm-task-status-last-change

<<src>>

(pg-org-deftest pm-task/status-last-change
    input/basic
  (-let (((from to timestamp notes)
          (->> (org-ml-match '(headline) doc)
               (nth 0)
               (pm-task-status-last-change))))
    (should (string-equal to "DOING"))
    (should (string-equal from "BLOCKED"))
    (should (ts= (ts-parse-org-element timestamp) (ts-parse-org "[2021-12-11 Sat 20:36]")))
    (should (string-equal (org-ml-to-trimmed-string notes) "Back on the case"))))

(pg-org-deftest pm-task/status-last-change-to
    input/basic
  (let ((to (->> (org-ml-match '(headline) doc)
                 (nth 0)
                 (pm-task-status-last-change-to))))
    (should (string-equal to "DOING"))))

(pg-org-deftest pm-task/status-last-change-from
    input/basic
  (let ((to (->> (org-ml-match '(headline) doc)
                 (nth 0)
                 (pm-task-status-last-change-from))))
    (should (string-equal to "BLOCKED"))))


(pg-org-deftest pm-task/status-last-change-timestamp
    input/basic
  (let ((timestamp (->> (org-ml-match '(headline) doc)
                        (nth 0)
                        (pm-task-status-last-change-timestamp))))
    (should (ts= timestamp (ts-parse-org "[2021-12-11 Sat 20:36]")))))

(pg-org-deftest pm-task/status-last-change-org-timestamp
    input/basic
  (let ((timestamp (->> (org-ml-match '(headline) doc)
                        (nth 0)
                        (pm-task-status-last-change-org-timestamp))))
    (should (ts= (ts-parse-org-element timestamp) (ts-parse-org "[2021-12-11 Sat 20:36]")))))


(pg-org-deftest pm-task/status-last-change-notes
    input/basic
  (let ((notes (->> (org-ml-match '(headline) doc)
                    (nth 0)
                    (pm-task-status-last-change-notes))))
    (should (string-equal (org-ml-to-trimmed-string notes) "Back on the case"))))

(pg-ert-run-tests-string "pm-task/status-last-change")

Testing: pm-task-current-status

This test uses an additional input, shown in Listing input/current-status. (Technically, a bare headline is not a task, but a core design principle of this library is to be liberal in what is accepted.)

* No status here
<<src/accessors/status>>
(pg-org-deftest pm-task/current-status/1
    input/basic
  (let ((status (->> (org-ml-match '(headline) doc)
                     (nth 0)
                     (pm-task-current-status))))
    (should (string-equal status "DOING"))))

(pg-org-deftest pm-task/current-status/2
    input/current-status
  (let ((task (->> (org-ml-match '(headline) doc)
                   (nth 0))))
    (should (eq nil (pm-task-current-status task)))))

(pg-ert-run-tests-string "pm-task/current-status")

Assignee

(defun pm-task-assignee (task)
  "Returns the user to whom the task is assigned. Returns `nil'
    if there is no assignee."
  (or (org-ml-headline-get-node-property "assignee" task)
      (org-ml-headline-get-node-property "ASSIGNEE" task)))

Testing: pm-task-assignee

<<src/accessors/assignee>>

(pg-org-deftest  pm-task/assignee
    input/basic
  (let ((assignee (->> (org-ml-match '(headline) doc)
                       (nth 0)
                       (pm-task-assignee))))
    (should (string-equal assignee "Bart Starr"))))

(pg-ert-run-tests-string "pm-task/assignee")

Creator and creation time

(defun pm-task-created-on (task)
  "Returns the time of this tasks creation, as a ts
    structure. Returns `nil' if TASK has no \"CREATED\" or
    \"created\" property drawer, or if the contents of that
    property are not a valid org-mode timestamp."
  (when-let* ((created (or (org-ml-headline-get-node-property "created" task)
                           (org-ml-headline-get-node-property "CREATED" task)))
              (created-ts (ts-parse-org created)))
    created-ts))

(defun pm-task-creator (task)
  "Returns the creator of this task, or `nil' if none is specified."
  (or (org-ml-headline-get-node-property "CREATOR" task)
      (org-ml-headline-get-node-property "creator" task)))

Testing: pm-task-created-on

<<src/accessors/creation>>

(pg-org-deftest  pm-task/created-on
    input/basic
  (let ((created (->> (org-ml-match '(headline) doc)
                      (nth 0)
                      (pm-task-created-on))))
    (should (ts= created (ts-parse-org "[2021-12-01 Wed 10:00]")))))

(pg-ert-run-tests-string "pm-task/created-on")

Testing: pm-task-creator

<<src/accessors/creation>>

(pg-org-deftest  pm-task/creator
    input/basic
  (let ((creator (->> (org-ml-match '(headline) doc)
                      (nth 0)
                      (pm-task-creator))))
    (should (string-equal creator "Vince Lombardi"))))

(pg-ert-run-tests-string "pm-task/creator")

Last change

The pm-task-last-change is different from a status change update; for that, see Section sct-status-changes, particularly pm-task-status-last-change-timestamp.

(defun pm-task-last-change (task)
  (cl-letf* (((symbol-function '-ts-val)
              (lambda (item)
                (-some->>
                    (org-ml-logbook-item-get-timestamp item)
                  (ts-parse-org-element))))

             ((symbol-function 'cmp)
              (lambda (l r)
                (let ((ts-l (-ts-val l))
                      (ts-r (-ts-val r)))
                  (and (and ts-l ts-r)
                       (ts< ts-l ts-r))))))
    (->> (pm-task-get-logbook task)
         (pg-org-logbook-get-items)
         (-sort 'cmp)
         (nth 0))))

Not implemented

One could certainly conceive of companions to the org-ml-logbook-item-get-timestamp function (which more or less returns the timestamp to the left of the line break, if it exists, for a logbook item) for the text to the left and right of the line break, which I’ve called the header and the message. If that were written, it would probably belong in pg-org.

Selecting and parsing tasks

Because a Task is an Org headline, it is easy to parse using Org’s org-element library or Org-ML, as shown in Listing ex/parsing-a-headline.

(pg-org-with-src-doc ex/task/2
  (->> doc
       (org-ml-match '(headline))
       (first)
       (org-ml-remove-parents)))

Using org-ml and pg-org, we can search through the headlines for items of interest and, where necessary, verify preconditions. To simplify the process further, this package defines these operations as a series of ypredicates in terms of tasks. These predicates permit the user to select tasks directly based on their task-related semantics, such as resolution times or assigned users.

Predicates

This is a work in progress. As predicates are defined to satisfy use cases in #sct-use-cases, they will be added here.

;; Everything has to deal with unsynced tasks. It's the caller's
;; responsibility to check if the task is synced before using.


(defun pm-task-is-synced (task)
  "Return `t' if TASK has a to-do item that matches the current
  state from the last entry in the logbook. If not, the logbook
  can't be used to determine the time of the most recent status
  change or the previous status."
  (let ((todo (org-ml-get-property :todo-keyword task)))
    (and todo
         (equal todo (pm-task-status-last-change-to task)))))

;; Time comparison

(defun pm-task-status-changed-on (timestamp task)
  "Return `t' if last logbook entry for TASK is equal to
TIMESTAMP, a ts object. If TASK is not synced, results are
undefined."
  (ts=  timestamp (pm-task-status-last-change-timestamp task)))

(defun pm-task-status-changed-after (timestamp task)
  "Return `t' if last logbook entry for TASK occured after
TIMESTAMP, a ts object. If TASK is not synced, results are
undefined."
  (ts< timestamp (pm-task-status-last-change-timestamp task)))

(defun pm-task-status-changed-on-or-after (timestamp task)
  "Return `t' if last logbook entry for TASK occured after
TIMESTAMP (a ts object) and task is synced. If TASK is not
synced, results are undefined."
  (ts<=  timestamp (pm-task-status-last-change-timestamp task)))

(defun pm-task-status-changed-before (timestamp task)
  "Return `t' if last logbook entry for TASK occured before
TIMESTAMP (a ts object) and task is synced. If TASK is not
synced, results are undefined."
  (ts> timestamp (pm-task-status-last-change-timestamp task)))

(defun pm-task-status-changed-on-or-before (timestamp task)
  "Return `t' if last logbook entry for TASK occured before
TIMESTAMP and task is synced. If TASK is not synced, results are
undefined."
  (ts>= timestamp (pm-task-status-last-change-timestamp task)))

;; Status comparison

(defun pm-task-status-in (status-or-statuses task)
  "Return `t' if the current status of TASK is one of the strings
  in STATUS-OR-STATUSES, which can be a single string or list of
  strings. if STATUS-OR-STATUSES is `nil' (or a list where one of
  its elements is nil), this function will return nil."
  (cond
   ((eq status-or-statuses nil)
    (eq nil (pm-task-current-status task)))
   ((stringp status-or-statuses)
    (s-equals-p status-or-statuses (pm-task-current-status task)))
   ((listp status-or-statuses)
    (--some (pm-task-status-in it task) status-or-statuses))
   (t (error "status-or-statuses must a string or list of strings"))))

;; Assignee

(defun pm-task-is-assigned-to (user-or-users task)
  "Return `t' if the assignee of TASK is in USER-OR-USERS, which
can be a single string or a list of strings. If USER-OR-USERS is
`nil' (or a list where one of its elements is nil), this function
will return `t' if TASK is unassigned."
  (cond
   ((eq user-or-users nil)
    (eq nil (pm-task-assignee task)))
   ((stringp user-or-users)
    (s-equals-p user-or-users (pm-task-assignee task)))
   ((listp user-or-users)
    (--some (pm-task-is-assigned-to it task) user-or-users))
   (t (error "user-or-users must a string or list of strings"))))

”**** Testing

pm-task-is-synced

This task uses its own test input, shown in Listing input/is-synced/1 and input/is-synced/2.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)

* DOING A Synced Task
  :LOGBOOK:
  - State "DOING"      from "TODO"       [2022-01-06 Thu 07:27] \\
    Started
  :END:

* DOING An unsynced task
#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)

* DOING An unsynced task
(pg-org-deftest pm-task/is-synced/1
    input/is-synced/1
  (-let (((task . _) (org-ml-match '(headline) doc)))
    (should (pm-task-is-synced task))))

(pg-org-deftest pm-task/is-synced/2
    input/is-synced/2
  (-let (((task . _) (org-ml-match '(headline) doc)))
    (should (not (pm-task-is-synced task)))))

(pg-ert-run-tests-string "pm-task/is-synced")
pm-task-status-changed-*

These functions are trivial extensions of pm-task-last-change-timestamp, so they were a low priority to test.

They were, naturally, among the ugliest functions to debug. Always test.

<<src>>


;; (pg-org-deftest pm-task/a-changed-on-story
;;     input/basic

;;   (let* ((truth-table '(nil t))
;;          (timestamp (ts-parse-org "[2021-12-11 Sat 20:36]"))
;;          (h1 (org-ml-match '((:and headline (:raw-value "Get past the guard")))))
;;          (h2 (org-ml-match '((:and headline (:raw-value "Rewire the security system")))))
;;          (r1 (pm-task-status-changed-on timestamp h1))
;;          (r2 (pm-task-status-changed-on timestamp h2)))
;;     (should (equal (nth 0 truth-table) r1))
;;     (should (equal (nth 1 truth-table) r2))))


(pg-org-deftest pm-task/status-changed-on
    input/basic
  (cl-macrolet
      ((|-
        (fn-name org-timestamp truth-table)
        `(let* (
                (timestamp (ts-parse-org ,org-timestamp))
                (h1 (org-ml-match
                     '((:and headline (:raw-value "Get past the guard")))
                     doc))
                (h2 (org-ml-match
                     '((:and headline (:raw-value "Rewire the security system")))
                     doc))
                (r1 (,fn-name timestamp (nth 0 h1)))
                (r2 (,fn-name timestamp (nth 0 h2))))
           ;; sensibility checks
           (should (equal (length h1) 1))
           (should (equal (length h2) 1))
           ;; tests
           (should (equal (nth 0 ,truth-table) r1))
           (should (equal (nth 1 ,truth-table) r2)))))

    ;; The chronological order of these dates, earliest to latest
    ;;
    ;;  - old
    ;;  - h2
    ;;  - h1
    ;;  - new
    ;;
    ;; (Yes, I know. But I'm repurposing a multipurpose input)

    (let ((old     "[2021-10-09 Sat 12:45]")    ;; date before all tasks
          (new     "[2022-01-09 Sun 12:45]")    ;; date after  all tasks
          (h1-date "[2021-12-31 Fri 17:48]")    ;; date of h1
          (h2-date "[2021-12-11 Sat 20:36]"))   ;; date of h2

      ;; changed-on
      (|- pm-task-status-changed-on h1-date '(t   nil))
      (|- pm-task-status-changed-on h2-date '(nil t))
      (|- pm-task-status-changed-on old     '(nil ))

      ;; changed-before
      (|- pm-task-status-changed-before new     '(t   t))
      (|- pm-task-status-changed-before old     '(nil nil))
      (|- pm-task-status-changed-before h1-date '(nil t))
      (|- pm-task-status-changed-before h2-date '(nil nil))

      ;; changed-on-or-before
      (|- pm-task-status-changed-on-or-before new     '(t   t))
      (|- pm-task-status-changed-on-or-before old     '(nil nil))
      (|- pm-task-status-changed-on-or-before h1-date '(t   t))
      (|- pm-task-status-changed-on-or-before h2-date '(nil ))

      ;; ;; changed-after
      (|- pm-task-status-changed-after new     '(nil nil))
      (|- pm-task-status-changed-after old     '(t   t))
      (|- pm-task-status-changed-after h1-date '(nil nil))
      (|- pm-task-status-changed-after h2-date '(t   nil))

      ;; ;; changed-on-or-after
      (|- pm-task-status-changed-on-or-after new     '(nil nil))
      (|- pm-task-status-changed-on-or-after old     '(t   t))
      (|- pm-task-status-changed-on-or-after h1-date '(t   nil))
      (|- pm-task-status-changed-on-or-after h2-date '(t   t)))))

(pg-ert-run-tests-string "pm-test/status-changed-on")
pm-task-status-in

Input shown in Listing input/status-in/1.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)
#
* TODO to-do
* DOING doing
* DONE done
<<src/predicates>>
(pg-org-deftest pm-task/status-in
    input/status-in/1
  (let* ((tasks (org-ml-match '(headline) doc))
         (to-do (-filter (-partial #'pm-task-status-in "TODO") tasks))
         (doing (-filter (-partial #'pm-task-status-in "DOING") tasks))
         (done (-filter (-partial #'pm-task-status-in "DONE") tasks))
         (to-do+doing (-filter
                       (-partial #'pm-task-status-in '("TODO" "DOING"))
                       tasks)))
    ;; to-do
    (should (= 1 (length to-do)))
    (should (string-equal
             "to-do" (->> (car to-do)
                          (org-ml-get-property :raw-value)
                          (org-ml-to-trimmed-string))))
    ;; doing
    (should (= 1 (length doing)))
    (should (string-equal
             "doing" (->> (car doing)
                          (org-ml-get-property :raw-value)
                          (org-ml-to-trimmed-string))))
    ;; done
    (should (= 1 (length done)))
    (should (string-equal
             "done" (->> (car done)
                         (org-ml-get-property :raw-value)
                         (org-ml-to-trimmed-string))))

    ;; to-do and doing
    (should (= 2 (length to-do+doing)))
    (should (equal (-concat to-do doing) to-do+doing))
))


(pg-ert-run-tests-string "pm-task/status-in")
pm-task-is-assigned-to

Input shown in Listing input/is-assigned-to.

* TODO Thing 1
  :PROPERTIES:
  :ASSIGNEE: Manny Ramirez
  :END:

* TODO Thing 2
  :PROPERTIES:
  :ASSIGNEE: Moe Howard
  :END:


* TODO Thing 3
  :PROPERTIES:
  :ASSIGNEE: Jack Lalanne
  :END:

* TODO Thing 4


* TODO Thing 5
  :PROPERTIES:
  :ASSIGNEE: Jack Lalanne
  :END:
<<src>>

(pg-org-deftest pm-task/is-assigned-to
    input/is-assigned-to

  (let* ((tasks (org-ml-match '(headline) doc))
         (manny (-filter (-partial
                          #'pm-task-is-assigned-to "Manny Ramirez")
                         tasks))
         (moe   (-filter (-partial
                          #'pm-task-is-assigned-to "Moe Howard")
                         tasks))
         (jack  (-filter (-partial
                          #'pm-task-is-assigned-to "Jack Lalanne")
                         tasks))
         (manny+moe (-filter (-partial
                              #'pm-task-is-assigned-to
                              '("Manny Ramirez" "Moe Howard"))
                             tasks)))

    (should (= 1 (length manny)))
    (should (string-equal
             "Thing 1" (->> (car manny)
                            (org-ml-get-property :raw-value)
                            (org-ml-to-trimmed-string))))

    (should (= 1 (length moe)))
    (should (string-equal
             "Thing 1" (->> (car manny)
                            (org-ml-get-property :raw-value)
                            (org-ml-to-trimmed-string))))

    (should (= 2 (length jack)))
    (should (equal '("Thing 3" "Thing 5")
                   (--map (->> (org-ml-get-property :raw-value it)
                               (org-ml-to-trimmed-string))
                          jack)))

    (should (= 2 (length manny+moe)))
    (should (equal (-concat manny moe) manny+moe))))


(pg-ert-run-tests-string "pm-task/is-assigned-to")

Use Cases

Let’s work through some use cases and see what we need to make them happen. The Org file in Listing input/basic is the input to most of these examples.

Synced and unsynced tasks

A task is said to be “synced” when its to-do item is identical to the “to” state of the most recent logbook entry. This is not a guarantee that the task is a complete record of all changes to the task, but the lack indicates that something is missing, and possibly that the task was not intended for processing by this package.

The code in listings ex/unsynced-tasks/1 and ex/unsynced-tasks/2 selects only unsynchronized tasks. This would most likely be used to find tasks that should be synced but aren’t, or to distinguish tasks for different kinds of processing.

One consideration is what to do with tasks that are in an initial state. Currently, nothing is done, but the right answer is probably to look for a CREATED property and assume the task is synced if it that is present.

We use Listing input/unsynced-tasks to test this, as input/basic lacks unsynced tasks.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :CREATED:  [2021-12-01 Wed 10:00]
  :CREATOR: Vince Lombardi
  :END:
  :LOGBOOK:
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:36] \\
    Back on the case
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:25] \\
    Waiting on parts from the supplier
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:14] \\
    In process, it's harder than it looks
  :END:

* DONE Get past the guard
  :LOGBOOK:
  - State "DONE"       from "TODO"       [2021-12-31 Fri 17:48] \\
    Got past 'em, easy peasy
  :END:

* TODO Hey how'd this get here?


<<src>>

(pg-org-with-src-doc input/unsynced-tasks
  (->> doc
       (pg-org-match '((:and headline
                             (:-pred (pm-task-is-synced el)))))
       (-map (-partial #'org-ml-get-property :raw-value))))
<<src>>

(pg-org-with-src-doc input/unsynced-tasks
  (->> doc
       (pg-org-match '((:and headline
                             (:-pred (not (pm-task-is-synced el))))))
       (-map (-partial #'org-ml-get-property :raw-value))))

Note that, in this case, nothing is gained by using pg-org-match; one can just as easily use (org-ml-match '((:and headline (:pred pm-task-is-synced)))). (Assuming use of the ->> macro as in the example.) The pg-org-match function is more useful when pm-task-is-synced and other predicates are used concurrently. It is often beneficial to use pm-task-is-synced in this way, to assure a task is synced before using other predicates that only make sense when applied to synchronized tasks.

Tasks created since τ

This use case requires that task creation be tracked.

Tasks updated since τ

This will return only the headline for “Get past the guard”, as the timestamp for that headline falls on the time being searched for.

<<src>>


(let ((timestamp (ts-parse-org "[2021-12-31 Fri 17:48]")))
  (pg-org-with-src-doc input/basic
    (->> doc
         (pg-org-match
          '((:and headline
                  (:-pred
                   (and (pm-task-is-synced el)
                        (pm-task-status-changed-on-or-after
                         timestamp el))))))
         (-map (-partial #'org-ml-get-property :raw-value)))))

Tasks resolved since τ

Each project has its own set of statuses, some of which can indicate that a task has been resolved in some way. It may make sense to relate project metadata to tasks at some point; one use for that is relating a task to the valid statuses for that task.

For now, we can just use pm-task-status-in and supply our own set of statuses.

For demonstration purposes, we will augment our example to include tasks with other statuses representing completion. The modified input is in Listing input/resolved-since.

#+seq_todo: TODO  DOING(@) BLOCKED(@) | DONE(@) CANCELLED(@)


* DOING Rewire the security system
  :PROPERTIES:
  :ASSIGNEE: Bart Starr
  :CREATED:  [2021-12-01 Wed 10:00]
  :CREATOR: Vince Lombardi
  :END:
  :LOGBOOK:
  - State "DOING"      from "BLOCKED"    [2021-12-11 Sat 20:36] \\
    Back on the case
  - State "BLOCKED"    from "DOING"      [2021-12-11 Sat 20:25] \\
    Waiting on parts from the supplier
  - State "DOING"      from "TODO"       [2021-12-11 Sat 20:14] \\
    In process, it's harder than it looks
  :END:

* CANCELLED Crack the safe
  :LOGBOOK:
  - State "CANCELLED"  from "TODO"       [2021-12-10 Fri 08:00] \\
    Safe's empty! We've been rumbled!
  :END:

* DONE Get past the guard
  :LOGBOOK:
  - State "DONE"       from "TODO"       [2021-12-31 Fri 17:48] \\
    Got past 'em, easy peasy
  :END:
<<src>>

;; t is a timestamp of interest (e.g., 7 days ago)

(let ((timestamp (ts-parse-org "[2021-12-13 Mon 07:00]")))
  (pg-org-with-src-doc input/resolved-since
    (->> doc
         (pg-org-match
          '((:and headline
                  (:-pred
                   (and (pm-task-is-synced el)
                        (pm-task-status-in '("DONE" "CANCELLED") el))))))
         (-map (-partial #'org-ml-get-property :raw-value)))))

Tasks assigned to υ

<<src>>

(pg-org-with-src-doc input/basic
  (->> doc
       (pg-org-match
        '((:and headline (:-pred (pm-task-is-assigned-to "Bart Starr" el)))))
       (-map (-partial #'org-ml-get-property :raw-value))))

` `

Tasks assigned to υ since τ

This use case requires that assignment changes be fully tracked, with timestamps for task creation and reassignment.

Tasks reassigned since τ

Tasks with status σ

(pg-org-with-src-doc input/basic
  (->> doc
       (pg-org-match
        '((:and headline (:-pred (pm-task-status-in '("DONE") el)))))
       (-map (-partial #'org-ml-get-property :raw-value))))

All unresolved tasks

UI

Adding annotations

src/ui/logbook/annotate

(defun pm-ui--annotate-finish-function ()
  (if (not org-note-abort)
      (let ((txt (buffer-substring-no-properties (point-min) (point-max)))
            lines)
        ;; Strip comments and format a la org-mode
        ;; (especially org-store-log-note)
        (while (string-match "\\`# .*\n[ \t\n]*" txt)
          (setq txt (replace-match "" t t txt)))
        (when (string-match "\\s-+\\'" txt)
          (setq txt (replace-match "" t t txt)))
        (message txt))
    (message "aborted"))
  (kill-buffer))

(defun pm-ui-annotate ()                ;(task)
  "Create annotation for TASK, editing content of message in a temporary buffer"
  (interactive)
  (org-switch-to-buffer-other-window "*PM Task Annotate*")
  (erase-buffer)
  (let ((org-inhibit-startup t)) (org-mode))
  (insert "# Insert annotation. Finish with C-c C-c, cancel with C-c C-k\n\n")
  (setq-local org-finish-function #'pm-ui--annotate-finish-function))


Provide

(provide 'pm-task)
;;; pm-task.el ends here