Merge branch 'master' of git+ssh://repo.or.cz/srv/git/org-mode
[org-mode.git] / contrib / lisp / org-depend.el
blobeb38aa03437848db3fa702ec895a360591b995b8
1 ;;; org-depend.el --- TODO dependencies for Org-mode
2 ;; Copyright (C) 2008 Free Software Foundation, Inc.
3 ;;
4 ;; Author: Carsten Dominik <carsten at orgmode dot org>
5 ;; Keywords: outlines, hypermedia, calendar, wp
6 ;; Homepage: http://orgmode.org
7 ;; Version: 0.08
8 ;;
9 ;; This file is not part of GNU Emacs.
11 ;; This file is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; any later version.
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs; see the file COPYING. If not, write to the
23 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 ;; Boston, MA 02110-1301, USA.
25 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
27 ;;; Commentary:
29 ;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
30 ;; of Org-mode.
32 ;; This is an example implementation of TODO dependencies in Org-mode.
33 ;; It uses the new hooks in version 5.13 of Org-mode,
34 ;; `org-trigger-hook' and `org-blocker-hook'.
36 ;; It implements the following:
38 ;; Triggering
39 ;; ----------
41 ;; 1) If an entry contains a TRIGGER property that contains the string
42 ;; "chain-siblings(KEYWORD)", then switching that entry to DONE does
43 ;; do the following:
44 ;; - The sibling following this entry switched to todo-state KEYWORD.
45 ;; - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
46 ;; property, to make sure that, when *it* is DONE, the chain will
47 ;; continue.
49 ;; 2) If an entry contains a TRIGGER property that contains the string
50 ;; "chain-siblings-scheduled", then switching that entry to DONE does
51 ;; the following actions, similarly to "chain-siblings(KEYWORD)":
52 ;; - The sibling receives the same scheduled time as the entry
53 ;; marked as DONE (or, in the case, in which there is no scheduled
54 ;; time, the sibling does not get any either).
55 ;; - The sibling also gets the same TRIGGER property
56 ;; "chain-siblings-scheduled", so the chain can continue.
58 ;; 3) If the TRIGGER property contains any other words like
59 ;; XYZ(KEYWORD), these are treated as entry id's with keywords. That
60 ;; means Org-mode will search for an entry with the ID property XYZ
61 ;; and switch that entry to KEYWORD as well.
63 ;; Blocking
64 ;; --------
66 ;; 1) If an entry contains a BLOCKER property that contains the word
67 ;; "previous-sibling", the sibling above the current entry is
68 ;; checked when you try to mark it DONE. If it is still in a TODO
69 ;; state, the current state change is blocked.
71 ;; 2) If the BLOCKER property contains any other words, these are
72 ;; treated as entry id's. That means Org-mode will search for an
73 ;; entry with the ID property exactly equal to this word. If any
74 ;; of these entries is not yet marked DONE, the current state change
75 ;; will be blocked.
77 ;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
78 ;; you can find the offending entry with `C-c &'.
80 ;;; Example:
82 ;; When trying this example, make sure that the settings for TODO keywords
83 ;; have been activated, i.e. include the following line and press C-c C-c
84 ;; on the line before working with the example:
86 ;; #+TYP_TODO: TODO NEXT | DONE
88 ;; * TODO Win a million in Las Vegas
89 ;; The "third" TODO (see above) cannot become a TODO without this money.
91 ;; :PROPERTIES:
92 ;; :ID: I-cannot-do-it-without-money
93 ;; :END:
95 ;; * Do this by doing a chain of TODO's
96 ;; ** NEXT This is the first in this chain
97 ;; :PROPERTIES:
98 ;; :TRIGGER: chain-siblings(NEXT)
99 ;; :END:
101 ;; ** This is the second in this chain
103 ;; ** This is the third in this chain
104 ;; :PROPERTIES:
105 ;; :BLOCKER: I-cannot-do-it-without-money
106 ;; :END:
108 ;; ** This is the forth in this chain
109 ;; When this is DONE, we will also trigger entry XYZ-is-my-id
110 ;; :PROPERTIES:
111 ;; :TRIGGER: XYZ-is-my-id(TODO)
112 ;; :END:
114 ;; ** This is the fifth in this chain
116 ;; * Start writing report
117 ;; :PROPERTIES:
118 ;; :ID: XYZ-is-my-id
119 ;; :END:
123 (require 'org)
125 (defcustom org-depend-tag-blocked t
126 "Whether to indicate blocked TODO items by a special tag."
127 :group 'org
128 :type 'boolean)
130 (defmacro org-depend-act-on-sibling (trigger-val &rest rest)
131 "Perform a set of actions on the next sibling, if it exists,
132 copying the sibling spec TRIGGER-VAL to the next sibling."
133 `(catch 'exit
134 (save-excursion
135 (goto-char pos)
136 ;; find the sibling, exit if no more siblings
137 (condition-case nil
138 (outline-forward-same-level 1)
139 (error (throw 'exit t)))
140 ;; mark the sibling TODO
141 ,@rest
142 ;; make sure the sibling will continue the chain
143 (org-entry-add-to-multivalued-property
144 nil "TRIGGER" ,trigger-val))))
146 (defun org-depend-trigger-todo (change-plist)
147 "Trigger new TODO entries after the current is switched to DONE.
148 This does two different kinds of triggers:
150 - If the current entry contains a TRIGGER property that contains
151 \"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
152 KEYWORD and also installs the \"chain-sibling\" trigger to continue
153 the chain.
154 - If the current entry contains a TRIGGER property that contains
155 \"chain-siblings-scheduled\", we go to the next sibling and copy
156 the scheduled time from the current task, also installing the property
157 in the sibling.
158 - Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
159 property is seen as an entry id. Org-mode finds the entry with the
160 corresponding ID property and switches it to the state TODO as well."
162 ;; Get information from the plist
163 (let* ((type (plist-get change-plist :type))
164 (pos (plist-get change-plist :position))
165 (from (plist-get change-plist :from))
166 (to (plist-get change-plist :to))
167 (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger!
168 trigger triggers tr p1 kwd)
169 (catch 'return
170 (unless (eq type 'todo-state-change)
171 ;; We are only handling todo-state-change....
172 (throw 'return t))
173 (unless (and (member from org-not-done-keywords)
174 (member to org-done-keywords))
175 ;; This is not a change from TODO to DONE, ignore it
176 (throw 'return t))
178 ;; OK, we just switched from a TODO state to a DONE state
179 ;; Lets see if this entry has a TRIGGER property.
180 ;; If yes, split it up on whitespace.
181 (setq trigger (org-entry-get pos "TRIGGER")
182 triggers (and trigger (org-split-string trigger "[ \t]+")))
184 ;; Go through all the triggers
185 (while (setq tr (pop triggers))
186 (cond
187 ((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
188 ;; This is a TODO chain of siblings
189 (setq kwd (match-string 1 tr))
190 (org-depend-act-on-sibling (format "chain-siblings(%s)" kwd)
191 (org-todo kwd)))
193 ((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
194 ;; This seems to be ENTRY_ID(KEYWORD)
195 (setq id (match-string 1 tr)
196 kwd (match-string 2 tr)
197 p1 (org-find-entry-with-id id))
198 (when p1
199 ;; there is an entry with this ID, mark it TODO
200 (save-excursion
201 (goto-char p1)
202 (org-todo kwd))))
203 ((string-match "\\`chain-siblings-scheduled\\'" tr)
204 (let ((time (org-get-scheduled-time pos)))
205 (when time
206 (org-depend-act-on-sibling
207 "chain-siblings-scheduled"
208 (org-schedule nil time))))))))))
210 (defun org-depend-block-todo (change-plist)
211 "Block turning an entry into a TODO.
212 This checks for a BLOCKER property in an entry and checks
213 all the entries listed there. If any of them is not done,
214 block changing the current entry into a TODO entry. If the property contains
215 the word \"previous-sibling\", the sibling above the current entry is checked.
216 Any other words are treated as entry id's. If an entry exists with the
217 this ID property, that entry is also checked."
218 ;; Get information from the plist
219 (let* ((type (plist-get change-plist :type))
220 (pos (plist-get change-plist :position))
221 (from (plist-get change-plist :from))
222 (to (plist-get change-plist :to))
223 (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger
224 blocker blockers bl p1
225 (proceed-p
226 (catch 'return
227 (unless (eq type 'todo-state-change)
228 ;; We are not handling this kind of change
229 (throw 'return t))
230 (unless (and (not from) (member to org-not-done-keywords))
231 ;; This is not a change from nothing to TODO, ignore it
232 (throw 'return t))
234 ;; OK, the plan is to switch from nothing to TODO
235 ;; Lets see if we will allow it. Find the BLOCKER property
236 ;; and split it on whitespace.
237 (setq blocker (org-entry-get pos "BLOCKER")
238 blockers (and blocker (org-split-string blocker "[ \t]+")))
240 ;; go through all the blockers
241 (while (setq bl (pop blockers))
242 (cond
243 ((equal bl "previous-sibling")
244 ;; the sibling is required to be DONE.
245 (catch 'ignore
246 (save-excursion
247 (goto-char pos)
248 ;; find the older sibling, exit if no more siblings
249 (condition-case nil
250 (outline-backward-same-level 1)
251 (error (throw 'ignore t)))
252 ;; Check if this entry is not yet done and block
253 (unless (org-entry-is-done-p)
254 ;; return nil, to indicate that we block the change!
255 (org-mark-ring-push)
256 (throw 'return nil)))))
258 ((setq p1 (org-find-entry-with-id bl))
259 ;; there is an entry with this ID, check it out
260 (save-excursion
261 (goto-char p1)
262 (unless (org-entry-is-done-p)
263 ;; return nil, to indicate that we block the change!
264 (org-mark-ring-push)
265 (throw 'return nil))))))
266 t ; return t to indicate that we are not blocking
268 (when org-depend-tag-blocked
269 (org-toggle-tag "blocked" (if proceed-p 'off 'on)))
271 proceed-p))
273 (add-hook 'org-trigger-hook 'org-depend-trigger-todo)
274 (add-hook 'org-blocker-hook 'org-depend-block-todo)
276 (provide 'org-depend)
278 ;;; org-depend.el ends here