1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
|
;;; org-depend.el --- TODO dependencies for Org-mode
;; Copyright (C) 2008-2018 Free Software Foundation, Inc.
;;
;; Author: Carsten Dominik <carsten at orgmode dot org>
;; Keywords: outlines, hypermedia, calendar, wp
;; Homepage: https://orgmode.org
;; Version: 0.08
;;
;; This file is not part of GNU Emacs.
;;
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Commentary:
;;
;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
;; of Org-mode.
;;
;; This is an example implementation of TODO dependencies in Org-mode.
;; It uses the new hooks in version 5.13 of Org-mode,
;; `org-trigger-hook' and `org-blocker-hook'.
;;
;; It implements the following:
;;
;; Triggering
;; ----------
;;
;; 1) If an entry contains a TRIGGER property that contains the string
;; "chain-siblings(KEYWORD)", then switching that entry to DONE does
;; do the following:
;; - The sibling following this entry switched to todo-state KEYWORD.
;; - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
;; property, to make sure that, when *it* is DONE, the chain will
;; continue.
;;
;; 2) If an entry contains a TRIGGER property that contains the string
;; "chain-siblings-scheduled", then switching that entry to DONE does
;; the following actions, similarly to "chain-siblings(KEYWORD)":
;; - The sibling receives the same scheduled time as the entry
;; marked as DONE (or, in the case, in which there is no scheduled
;; time, the sibling does not get any either).
;; - The sibling also gets the same TRIGGER property
;; "chain-siblings-scheduled", so the chain can continue.
;;
;; 3) If the TRIGGER property contains the string
;; "chain-find-next(KEYWORD[,OPTIONS])", then switching that entry
;; to DONE do the following:
;; - All siblings are of the entry are collected into a temporary
;; list and then filtered and sorted according to OPTIONS
;; - The first sibling on the list is changed into KEYWORD state
;; - The sibling also gets the same TRIGGER property
;; "chain-find-next", so the chain can continue.
;;
;; OPTIONS should be a comma separated string without spaces, and
;; can contain following options:
;;
;; - from-top the candidate list is all of the siblings in
;; the current subtree
;;
;; - from-bottom candidate list are all siblings from bottom up
;;
;; - from-current candidate list are all siblings from current item
;; until end of subtree, then wrapped around from
;; first sibling
;;
;; - no-wrap candidate list are siblings from current one down
;;
;; - todo-only Only consider siblings that have a todo keyword
;; -
;; - todo-and-done-only
;; Same as above but also include done items.
;;
;; - priority-up sort by highest priority
;; - priority-down sort by lowest priority
;; - effort-up sort by highest effort
;; - effort-down sort by lowest effort
;;
;; Default OPTIONS are from-top
;;
;;
;; 4) If the TRIGGER property contains any other words like
;; XYZ(KEYWORD), these are treated as entry id's with keywords. That
;; means Org-mode will search for an entry with the ID property XYZ
;; and switch that entry to KEYWORD as well.
;;
;; Blocking
;; --------
;;
;; 1) If an entry contains a BLOCKER property that contains the word
;; "previous-sibling", the sibling above the current entry is
;; checked when you try to mark it DONE. If it is still in a TODO
;; state, the current state change is blocked.
;;
;; 2) If the BLOCKER property contains any other words, these are
;; treated as entry id's. That means Org-mode will search for an
;; entry with the ID property exactly equal to this word. If any
;; of these entries is not yet marked DONE, the current state change
;; will be blocked.
;;
;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
;; you can find the offending entry with `C-c &'.
;;
;;; Example:
;;
;; When trying this example, make sure that the settings for TODO keywords
;; have been activated, i.e. include the following line and press C-c C-c
;; on the line before working with the example:
;;
;; #+TYP_TODO: TODO NEXT | DONE
;;
;; * TODO Win a million in Las Vegas
;; The "third" TODO (see above) cannot become a TODO without this money.
;;
;; :PROPERTIES:
;; :ID: I-cannot-do-it-without-money
;; :END:
;;
;; * Do this by doing a chain of TODO's
;; ** NEXT This is the first in this chain
;; :PROPERTIES:
;; :TRIGGER: chain-siblings(NEXT)
;; :END:
;;
;; ** This is the second in this chain
;;
;; ** This is the third in this chain
;; :PROPERTIES:
;; :BLOCKER: I-cannot-do-it-without-money
;; :END:
;;
;; ** This is the forth in this chain
;; When this is DONE, we will also trigger entry XYZ-is-my-id
;; :PROPERTIES:
;; :TRIGGER: XYZ-is-my-id(TODO)
;; :END:
;;
;; ** This is the fifth in this chain
;;
;; * Start writing report
;; :PROPERTIES:
;; :ID: XYZ-is-my-id
;; :END:
;;
;;
(require 'org)
(eval-when-compile
(require 'cl))
(defcustom org-depend-tag-blocked t
"Whether to indicate blocked TODO items by a special tag."
:group 'org
:type 'boolean)
(defcustom org-depend-find-next-options
"from-current,todo-only,priority-up"
"Default options for chain-find-next trigger"
:group 'org
:type 'string)
(defmacro org-depend-act-on-sibling (trigger-val &rest rest)
"Perform a set of actions on the next sibling, if it exists,
copying the sibling spec TRIGGER-VAL to the next sibling."
`(catch 'exit
(save-excursion
(goto-char pos)
;; find the sibling, exit if no more siblings
(condition-case nil
(outline-forward-same-level 1)
(error (throw 'exit t)))
;; mark the sibling TODO
,@rest
;; make sure the sibling will continue the chain
(org-entry-add-to-multivalued-property
nil "TRIGGER" ,trigger-val))))
(defvar org-depend-doing-chain-find-next nil)
(defun org-depend-trigger-todo (change-plist)
"Trigger new TODO entries after the current is switched to DONE.
This does two different kinds of triggers:
- If the current entry contains a TRIGGER property that contains
\"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
KEYWORD and also installs the \"chain-sibling\" trigger to continue
the chain.
- If the current entry contains a TRIGGER property that contains
\"chain-siblings-scheduled\", we go to the next sibling and copy
the scheduled time from the current task, also installing the property
in the sibling.
- Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
property is seen as an entry id. Org-mode finds the entry with the
corresponding ID property and switches it to the state TODO as well."
;; Refresh the effort text properties
(org-refresh-properties org-effort-property 'org-effort)
;; Get information from the plist
(let* ((type (plist-get change-plist :type))
(pos (plist-get change-plist :position))
(from (plist-get change-plist :from))
(to (plist-get change-plist :to))
(org-log-done nil) ; IMPROTANT!: no logging during automatic trigger!
trigger triggers tr p1 p2 kwd id)
(catch 'return
(unless (eq type 'todo-state-change)
;; We are only handling todo-state-change....
(throw 'return t))
(unless (and (member from org-not-done-keywords)
(member to org-done-keywords))
;; This is not a change from TODO to DONE, ignore it
(throw 'return t))
;; OK, we just switched from a TODO state to a DONE state
;; Lets see if this entry has a TRIGGER property.
;; If yes, split it up on whitespace.
(setq trigger (org-entry-get pos "TRIGGER")
triggers (and trigger (split-string trigger)))
;; Go through all the triggers
(while (setq tr (pop triggers))
(cond
((and (not org-depend-doing-chain-find-next)
(string-match "\\`chain-find-next(\\b\\(.+?\\)\\b\\(.*\\))\\'" tr))
;; smarter sibling selection
(let* ((org-depend-doing-chain-find-next t)
(kwd (match-string 1 tr))
(options (match-string 2 tr))
(options (if (or (null options)
(equal options ""))
org-depend-find-next-options
options))
(todo-only (string-match "todo-only" options))
(todo-and-done-only (string-match "todo-and-done-only"
options))
(from-top (string-match "from-top" options))
(from-bottom (string-match "from-bottom" options))
(from-current (string-match "from-current" options))
(no-wrap (string-match "no-wrap" options))
(priority-up (string-match "priority-up" options))
(priority-down (string-match "priority-down" options))
(effort-up (string-match "effort-up" options))
(effort-down (string-match "effort-down" options)))
(save-excursion
(org-back-to-heading t)
(let ((this-item (point)))
;; go up to the parent headline, then advance to next child
(org-up-heading-safe)
(let ((end (save-excursion (org-end-of-subtree t)
(point)))
(done nil)
(items '()))
(outline-next-heading)
(while (not done)
(if (not (looking-at org-complex-heading-regexp))
(setq done t)
(let ((todo-kwd (match-string 2))
(tags (match-string 5))
(priority (org-get-priority (or (match-string 3) "")))
(effort (when (or effort-up effort-down)
(let ((effort (get-text-property (point) 'org-effort)))
(when effort
(org-duration-to-minutes effort))))))
(push (list (point) todo-kwd priority tags effort)
items))
(unless (org-goto-sibling)
(setq done t))))
;; massage the list according to options
(setq items
(cond (from-top (nreverse items))
(from-bottom items)
((or from-current no-wrap)
(let* ((items (nreverse items))
(pos (position this-item items :key #'first))
(items-before (subseq items 0 pos))
(items-after (subseq items pos)))
(if no-wrap items-after
(append items-after items-before))))
(t (nreverse items))))
(setq items (remove-if
(lambda (item)
(or (equal (first item) this-item)
(and (not todo-and-done-only)
(member (second item) org-done-keywords))
(and (or todo-only
todo-and-done-only)
(null (second item)))))
items))
(setq items
(sort
items
(lambda (item1 item2)
(let* ((p1 (third item1))
(p2 (third item2))
(e1 (fifth item1))
(e2 (fifth item2))
(p1-lt (< p1 p2))
(p1-gt (> p1 p2))
(e1-lt (and e1 (or (not e2) (< e1 e2))))
(e2-gt (and e2 (or (not e1) (> e1 e2)))))
(cond (priority-up
(or p1-gt
(and (equal p1 p2)
(or (and effort-up e1-lt)
(and effort-down e2-gt)))))
(priority-down
(or p1-lt
(and (equal p1 p2)
(or (and effort-up e1-lt)
(and effort-down e2-gt)))))
(effort-up
(or e2-gt (and (equal e1 e2) p1-gt)))
(effort-down
(or e1-lt (and (equal e1 e2) p1-gt))))))))
(when items
(goto-char (first (first items)))
(org-entry-add-to-multivalued-property nil "TRIGGER" tr)
(org-todo kwd)))))))
((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
;; This is a TODO chain of siblings
(setq kwd (match-string 1 tr))
(org-depend-act-on-sibling (format "chain-siblings(%s)" kwd)
(org-todo kwd)))
((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
;; This seems to be ENTRY_ID(KEYWORD)
(setq id (match-string 1 tr)
kwd (match-string 2 tr)
p1 (org-find-entry-with-id id))
;; First check current buffer, then all files.
(if p1
;; There is an entry with this ID, mark it TODO.
(save-excursion
(goto-char p1)
(org-todo kwd))
(when (setq p2 (org-id-find id))
(save-excursion
(with-current-buffer (find-file-noselect (car p2))
(goto-char (cdr p2))
(org-todo kwd))))))
((string-match "\\`chain-siblings-scheduled\\'" tr)
(let ((time (org-get-scheduled-time pos)))
(when time
(org-depend-act-on-sibling
"chain-siblings-scheduled"
(org-schedule nil time))))))))))
(defun org-depend-block-todo (change-plist)
"Block turning an entry into a TODO.
This checks for a BLOCKER property in an entry and checks
all the entries listed there. If any of them is not done,
block changing the current entry into a TODO entry. If the property contains
the word \"previous-sibling\", the sibling above the current entry is checked.
Any other words are treated as entry id's. If an entry exists with the
this ID property, that entry is also checked."
;; Get information from the plist
(let* ((type (plist-get change-plist :type))
(pos (plist-get change-plist :position))
(from (plist-get change-plist :from))
(to (plist-get change-plist :to))
(org-log-done nil) ; IMPROTANT!: no logging during automatic trigger
blocker blockers bl p1 p2
(proceed-p
(catch 'return
;; If this is not a todo state change, or if this entry is
;; DONE, do not block
(when (or (not (eq type 'todo-state-change))
(member from (cons 'done org-done-keywords))
(member to (cons 'todo org-not-done-keywords))
(not to))
(throw 'return t))
;; OK, the plan is to switch from nothing to TODO
;; Lets see if we will allow it. Find the BLOCKER property
;; and split it on whitespace.
(setq blocker (org-entry-get pos "BLOCKER")
blockers (and blocker (split-string blocker)))
;; go through all the blockers
(while (setq bl (pop blockers))
(cond
((equal bl "previous-sibling")
;; the sibling is required to be DONE.
(catch 'ignore
(save-excursion
(goto-char pos)
;; find the older sibling, exit if no more siblings
(unless (org-get-last-sibling)
(throw 'ignore t))
;; Check if this entry is not yet done and block
(unless (org-entry-is-done-p)
;; return nil, to indicate that we block the change!
(org-mark-ring-push)
(throw 'return nil)))))
((setq p1 (org-find-entry-with-id bl))
;; there is an entry with this ID, check it out
(save-excursion
(goto-char p1)
(unless (org-entry-is-done-p)
;; return nil, to indicate that we block the change!
(org-mark-ring-push)
(throw 'return nil))))
((setq p2 (org-id-find bl))
(save-excursion
(with-current-buffer (find-file-noselect (car p2))
(goto-char (cdr p2))
(unless (org-entry-is-done-p)
(org-mark-ring-push)
(throw 'return nil)))))))
;; Return t to indicate that we are not blocking.
t)))
(when org-depend-tag-blocked
(org-toggle-tag "blocked" (if proceed-p 'off 'on)))
proceed-p))
(add-hook 'org-trigger-hook 'org-depend-trigger-todo)
(add-hook 'org-blocker-hook 'org-depend-block-todo)
(provide 'org-depend)
;;; org-depend.el ends here
|