Commit | Line | Data |
---|---|---|
86fbb8ca CD |
1 | ;;; org-taskjuggler.el --- TaskJuggler exporter for org-mode |
2 | ;; | |
3 | ;; Copyright (C) 2007, 2008, 2009, 2010 Free Software Foundation, Inc. | |
4 | ;; | |
5 | ;; Emacs Lisp Archive Entry | |
6 | ;; Filename: org-taskjuggler.el | |
7 | ;; Version: 7.01 | |
8 | ;; Author: Christian Egli | |
9 | ;; Maintainer: Christian Egli | |
10 | ;; Keywords: org, taskjuggler, project planning | |
11 | ;; Description: Converts an org-mode buffer into a taskjuggler project plan | |
12 | ;; URL: | |
13 | ||
14 | ;; This file is part of GNU Emacs. | |
15 | ||
16 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
17 | ;; it under the terms of the GNU General Public License as published by | |
18 | ;; the Free Software Foundation, either version 3 of the License, or | |
19 | ;; (at your option) any later version. | |
20 | ||
21 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
24 | ;; GNU General Public License for more details. | |
25 | ||
26 | ;; You should have received a copy of the GNU General Public License | |
27 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
28 | ||
29 | ;; Commentary: | |
30 | ;; | |
31 | ;; This library implements a TaskJuggler exporter for org-mode. | |
32 | ;; TaskJuggler uses a text format to define projects, tasks and | |
33 | ;; resources, so it is a natural fit for org-mode. It can produce all | |
34 | ;; sorts of reports for tasks or resources in either HTML, CSV or PDF. | |
35 | ;; The current version of TaskJuggler requires KDE but the next | |
36 | ;; version is implemented in Ruby and should therefore run on any | |
37 | ;; platform. | |
38 | ;; | |
39 | ;; The exporter is a bit different from other exporters, such as the | |
40 | ;; HTML and LaTeX exporters for example, in that it does not export | |
41 | ;; all the nodes of a document or strictly follow the order of the | |
42 | ;; nodes in the document. | |
43 | ;; | |
44 | ;; Instead the TaskJuggler exporter looks for a tree that defines the | |
45 | ;; tasks and a optionally tree that defines the resources for this | |
46 | ;; project. It then creates a TaskJuggler file based on these trees | |
47 | ;; and the attributes defined in all the nodes. | |
48 | ;; | |
49 | ;; * Installation | |
50 | ;; | |
51 | ;; Put this file into your load-path and the following line into your | |
52 | ;; ~/.emacs: | |
53 | ;; | |
54 | ;; (require 'org-taskjuggler) | |
55 | ;; | |
56 | ;; The interactive functions are similar to those of the HTML and LaTeX | |
57 | ;; exporters: | |
58 | ;; | |
59 | ;; M-x `org-export-as-taskjuggler' | |
60 | ;; M-x `org-export-as-taskjuggler-and-open' | |
61 | ;; | |
62 | ;; * Tasks | |
63 | ;; | |
64 | ;; Let's illustrate the usage with a small example. Create your tasks | |
65 | ;; as you usually do with org-mode. Assign efforts to each task using | |
66 | ;; properties (it's easiest to do this in the column view). You should | |
67 | ;; end up with something similar to the example by Peter Jones in | |
68 | ;; http://www.contextualdevelopment.com/static/artifacts/articles/2008/project-planning/project-planning.org. | |
69 | ;; Now mark the top node of your tasks with a tag named | |
70 | ;; "taskjuggler_project" (or whatever you customized | |
71 | ;; `org-export-taskjuggler-project-tag' to). You are now ready to | |
72 | ;; export the project plan with `org-export-as-taskjuggler-and-open' | |
73 | ;; which will export the project plan and open a gant chart in | |
74 | ;; TaskJugglerUI. | |
75 | ;; | |
76 | ;; * Resources | |
77 | ;; | |
78 | ;; Next you can define resources and assign those to work on specific | |
79 | ;; tasks. You can group your resources hierarchically. Tag the top | |
80 | ;; node of the resources with "taskjuggler_resource" (or whatever you | |
81 | ;; customized `org-export-taskjuggler-resource-tag' to). You can | |
82 | ;; optionally assign an identifier (named "resource_id") to the | |
83 | ;; resources (using the standard org properties commands) or you can | |
84 | ;; let the exporter generate identifiers automatically (the exporter | |
85 | ;; picks the first word of the headline as the identifier as long as | |
86 | ;; it is unique, see the documentation of | |
87 | ;; `org-taskjuggler-get-unique-id'). Using that identifier you can | |
88 | ;; then allocate resources to tasks. This is again done with the | |
89 | ;; "allocate" property on the tasks. Do this in column view or when on | |
90 | ;; the task type | |
91 | ;; | |
92 | ;; C-c C-x p allocate RET <resource_id> RET | |
93 | ;; | |
94 | ;; Once the allocations are done you can again export to TaskJuggler | |
95 | ;; and check in the Resource Allocation Graph which person is working | |
96 | ;; on what task at what time. | |
97 | ;; | |
98 | ;; * Export of properties | |
99 | ;; | |
100 | ;; The exporter also takes TODO state information into consideration, | |
101 | ;; i.e. if a task is marked as done it will have the corresponding | |
102 | ;; attribute in TaskJuggler ("complete 100"). Also it will export any | |
103 | ;; property on a task resource or resource node which is known to | |
104 | ;; TaskJuggler, such as limits, vacation, shift, booking, efficiency, | |
105 | ;; journalentry, rate for resources or account, start, note, duration, | |
106 | ;; end, journalentry, milestone, reference, responsible, scheduling, | |
107 | ;; etc for tasks. | |
108 | ;; | |
109 | ;; * Dependencies | |
110 | ;; | |
111 | ;; The exporter will handle dependencies that are defined in the tasks | |
112 | ;; either with the ORDERED attribute (see TODO dependencies in the Org | |
113 | ;; mode manual) or with the BLOCKER attribute (see org-depend.el) or | |
114 | ;; alternatively with a depends attribute. Both the BLOCKER and the | |
115 | ;; depends attribute can be either "previous-sibling" or a reference | |
116 | ;; to an identifier (named "task_id") which is defined for another | |
117 | ;; task in the project. BLOCKER and the depends attribute can define | |
118 | ;; multiple dependencies separated by either space or comma. You can | |
119 | ;; also specify optional attributes on the dependency by simply | |
120 | ;; appending it. The following examples should illustrate this: | |
121 | ;; | |
122 | ;; * Training material | |
123 | ;; :PROPERTIES: | |
124 | ;; :task_id: training_material | |
125 | ;; :ORDERED: t | |
126 | ;; :END: | |
127 | ;; ** Markup Guidelines | |
128 | ;; :PROPERTIES: | |
129 | ;; :Effort: 2.0 | |
130 | ;; :END: | |
131 | ;; ** Workflow Guidelines | |
132 | ;; :PROPERTIES: | |
133 | ;; :Effort: 2.0 | |
134 | ;; :END: | |
135 | ;; * Presentation | |
136 | ;; :PROPERTIES: | |
137 | ;; :Effort: 2.0 | |
138 | ;; :BLOCKER: training_material { gapduration 1d } some_other_task | |
139 | ;; :END: | |
140 | ;; | |
141 | ;;;; * TODO | |
142 | ;; - Use SCHEDULED and DEADLINE information (not just start and end | |
143 | ;; properties). | |
144 | ;; - Look at org-file-properties, org-global-properties and | |
145 | ;; org-global-properties-fixed | |
146 | ;; - What about property inheritance and org-property-inherit-p? | |
147 | ;; - Use TYPE_TODO as an way to assign resources | |
148 | ;; - Make sure multiple dependency definitions (i.e. BLOCKER on | |
149 | ;; previous-sibling and on a specific task_id) in multiple | |
150 | ;; attributes are properly exported. | |
151 | ;; | |
152 | ;;; Code: | |
153 | ||
154 | (eval-when-compile | |
155 | (require 'cl)) | |
156 | ||
157 | (require 'org) | |
158 | (require 'org-exp) | |
159 | ||
160 | ;;; User variables: | |
161 | ||
162 | (defgroup org-export-taskjuggler nil | |
163 | "Options for exporting Org-mode files to TaskJuggler." | |
164 | :tag "Org Export TaskJuggler" | |
165 | :group 'org-export) | |
166 | ||
167 | (defcustom org-export-taskjuggler-extension ".tjp" | |
168 | "Extension of TaskJuggler files." | |
169 | :group 'org-export-taskjuggler | |
170 | :type 'string) | |
171 | ||
172 | (defcustom org-export-taskjuggler-project-tag "taskjuggler_project" | |
173 | "Tag, property or todo used to find the tree containing all | |
174 | the tasks for the project." | |
175 | :group 'org-export-taskjuggler | |
176 | :type 'string) | |
177 | ||
178 | (defcustom org-export-taskjuggler-resource-tag "taskjuggler_resource" | |
179 | "Tag, property or todo used to find the tree containing all the | |
180 | resources for the project." | |
181 | :group 'org-export-taskjuggler | |
182 | :type 'string) | |
183 | ||
184 | (defcustom org-export-taskjuggler-default-project-version "1.0" | |
185 | "Default version string for the project." | |
186 | :group 'org-export-taskjuggler | |
187 | :type 'string) | |
188 | ||
189 | (defcustom org-export-taskjuggler-default-project-duration 280 | |
190 | "Default project duration if no start and end date have been defined | |
191 | in the root node of the task tree, i.e. the tree that has been marked | |
192 | with `org-export-taskjuggler-project-tag'" | |
193 | :group 'org-export-taskjuggler | |
194 | :type 'integer) | |
195 | ||
196 | (defcustom org-export-taskjuggler-default-reports | |
197 | '("taskreport \"Gantt Chart\" { | |
198 | headline \"Project Gantt Chart\" | |
199 | columns hierarchindex, name, start, end, effort, duration, completed, chart | |
200 | timeformat \"%Y-%m-%d\" | |
201 | hideresource 1 | |
202 | loadunit shortauto | |
203 | }" | |
204 | "resourcereport \"Resource Graph\" { | |
205 | headline \"Resource Allocation Graph\" | |
206 | columns no, name, utilization, freeload, chart | |
207 | loadunit shortauto | |
208 | sorttasks startup | |
209 | hidetask ~isleaf() | |
210 | }") | |
211 | "Default reports for the project." | |
212 | :group 'org-export-taskjuggler | |
213 | :type '(repeat (string :tag "Report"))) | |
214 | ||
215 | (defcustom org-export-taskjuggler-default-global-properties | |
216 | "shift s40 \"Part time shift\" { | |
217 | workinghours wed, thu, fri off | |
218 | } | |
219 | " | |
220 | "Default global properties for the project. Here you typically | |
221 | define global properties such as shifts, accounts, rates, | |
222 | vacation, macros and flags. Any property that is allowed within | |
223 | the TaskJuggler file can be inserted. You could for example | |
224 | include another TaskJuggler file. | |
225 | ||
226 | The global properties are inserted after the project declaration | |
227 | but before any resource and task declarations." | |
228 | :group 'org-export-taskjuggler | |
229 | :type '(string :tag "Preamble")) | |
230 | ||
231 | ;;; Hooks | |
232 | ||
233 | (defvar org-export-taskjuggler-final-hook nil | |
234 | "Hook run at the end of TaskJuggler export, in the new buffer.") | |
235 | ||
236 | ;;; Autoload functions: | |
237 | ||
238 | ;; avoid compiler warning about free variable | |
239 | (defvar org-export-taskjuggler-old-level) | |
240 | ||
241 | ;;;###autoload | |
242 | (defun org-export-as-taskjuggler () | |
243 | "Export parts of the current buffer as a TaskJuggler file. | |
244 | The exporter looks for a tree with tag, property or todo that | |
245 | matches `org-export-taskjuggler-project-tag' and takes this as | |
246 | the tasks for this project. The first node of this tree defines | |
247 | the project properties such as project name and project period. | |
248 | If there is a tree with tag, property or todo that matches | |
249 | `org-export-taskjuggler-resource-tag' this three is taken as | |
250 | resources for the project. If no resources are specified, a | |
251 | default resource is created and allocated to the project. Also | |
252 | the taskjuggler project will be created with default reports as | |
253 | defined in `org-export-taskjuggler-default-reports'." | |
254 | (interactive) | |
255 | ||
256 | (message "Exporting...") | |
257 | (setq-default org-done-keywords org-done-keywords) | |
258 | (let* ((tasks | |
259 | (org-taskjuggler-resolve-dependencies | |
260 | (org-taskjuggler-assign-task-ids | |
261 | (org-map-entries | |
262 | '(org-taskjuggler-components) | |
263 | org-export-taskjuggler-project-tag nil 'archive 'comment)))) | |
264 | (resources | |
265 | (org-taskjuggler-assign-resource-ids | |
266 | (org-map-entries | |
267 | '(org-taskjuggler-components) | |
268 | org-export-taskjuggler-resource-tag nil 'archive 'comment))) | |
269 | (filename (expand-file-name | |
270 | (concat | |
271 | (file-name-sans-extension | |
272 | (file-name-nondirectory buffer-file-name)) | |
273 | org-export-taskjuggler-extension))) | |
274 | (buffer (find-file-noselect filename)) | |
275 | (org-export-taskjuggler-old-level 0) | |
276 | task resource) | |
277 | (unless tasks | |
278 | (error "No tasks specified")) | |
279 | ;; add a default resource | |
280 | (unless resources | |
281 | (setq resources | |
282 | `((("resource_id" . ,(user-login-name)) | |
283 | ("headline" . ,user-full-name) | |
284 | ("level" . 1))))) | |
285 | ;; add a default allocation to the first task if none was given | |
286 | (unless (assoc "allocate" (car tasks)) | |
287 | (let ((task (car tasks)) | |
288 | (resource-id (cdr (assoc "resource_id" (car resources))))) | |
289 | (setcar tasks (push (cons "allocate" resource-id) task)))) | |
290 | ;; add a default start date to the first task if none was given | |
291 | (unless (assoc "start" (car tasks)) | |
292 | (let ((task (car tasks)) | |
293 | (time-string (format-time-string "%Y-%m-%d"))) | |
294 | (setcar tasks (push (cons "start" time-string) task)))) | |
295 | ;; add a default version if none was given | |
296 | (unless (assoc "version" (car tasks)) | |
297 | (let ((task (car tasks)) | |
298 | (version org-export-taskjuggler-default-project-version)) | |
299 | (setcar tasks (push (cons "version" version) task)))) | |
300 | (with-current-buffer buffer | |
301 | (erase-buffer) | |
302 | (org-taskjuggler-open-project (car tasks)) | |
303 | (insert org-export-taskjuggler-default-global-properties) | |
304 | (insert "\n") | |
305 | (dolist (resource resources) | |
306 | (let ((level (cdr (assoc "level" resource)))) | |
307 | (org-taskjuggler-close-maybe level) | |
308 | (org-taskjuggler-open-resource resource) | |
309 | (setq org-export-taskjuggler-old-level level))) | |
310 | (org-taskjuggler-close-maybe 1) | |
311 | (setq org-export-taskjuggler-old-level 0) | |
312 | (dolist (task tasks) | |
313 | (let ((level (cdr (assoc "level" task)))) | |
314 | (org-taskjuggler-close-maybe level) | |
315 | (org-taskjuggler-open-task task) | |
316 | (setq org-export-taskjuggler-old-level level))) | |
317 | (org-taskjuggler-close-maybe 1) | |
318 | (org-taskjuggler-insert-reports) | |
319 | (save-buffer) | |
320 | (or (org-export-push-to-kill-ring "TaskJuggler") | |
321 | (message "Exporting... done")) | |
322 | (current-buffer)))) | |
323 | ||
324 | ;;;###autoload | |
325 | (defun org-export-as-taskjuggler-and-open () | |
326 | "Export the current buffer as a TaskJuggler file and open it | |
327 | with the TaskJuggler GUI." | |
328 | (interactive) | |
329 | (let* ((file-name (buffer-file-name (org-export-as-taskjuggler))) | |
330 | (process-name "TaskJugglerUI") | |
331 | (command (concat process-name " " file-name))) | |
332 | (start-process-shell-command process-name nil command))) | |
333 | ||
334 | (defun org-taskjuggler-parent-is-ordered-p () | |
335 | "Return true if the parent of the current node has a property | |
336 | \"ORDERED\". Return nil otherwise." | |
337 | (save-excursion | |
338 | (and (org-up-heading-safe) (org-entry-get (point) "ORDERED")))) | |
339 | ||
340 | (defun org-taskjuggler-components () | |
341 | "Return an alist containing all the pertinent information for | |
342 | the current node such as the headline, the level, todo state | |
343 | information, all the properties, etc." | |
344 | (let* ((props (org-entry-properties)) | |
345 | (components (org-heading-components)) | |
346 | (level (nth 1 components)) | |
347 | (headline (nth 4 components)) | |
348 | (parent-ordered (org-taskjuggler-parent-is-ordered-p))) | |
349 | (push (cons "level" level) props) | |
350 | (push (cons "headline" headline) props) | |
351 | (push (cons "parent-ordered" parent-ordered) props))) | |
352 | ||
353 | (defun org-taskjuggler-assign-task-ids (tasks) | |
354 | "Given a list of tasks return the same list assigning a unique id | |
355 | and the full path to each task. Taskjuggler takes hierarchical ids. | |
356 | For that reason we have to make ids locally unique and we have to keep | |
357 | a path to the current task." | |
358 | (let ((previous-level 0) | |
359 | unique-ids unique-id | |
360 | path | |
361 | task resolved-tasks tmp) | |
362 | (dolist (task tasks resolved-tasks) | |
363 | (let ((level (cdr (assoc "level" task)))) | |
364 | (cond | |
365 | ((< previous-level level) | |
366 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) | |
367 | (dotimes (tmp (- level previous-level)) | |
368 | (push (list unique-id) unique-ids) | |
369 | (push unique-id path))) | |
370 | ((= previous-level level) | |
371 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) | |
372 | (push unique-id (car unique-ids)) | |
373 | (setcar path unique-id)) | |
374 | ((> previous-level level) | |
375 | (dotimes (tmp (- previous-level level)) | |
376 | (pop unique-ids) | |
377 | (pop path)) | |
378 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) | |
379 | (push unique-id (car unique-ids)) | |
380 | (setcar path unique-id))) | |
381 | (push (cons "unique-id" unique-id) task) | |
382 | (push (cons "path" (mapconcat 'identity (reverse path) ".")) task) | |
383 | (setq previous-level level) | |
384 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
385 | ||
386 | (defun org-taskjuggler-assign-resource-ids (resources &optional unique-ids) | |
387 | "Given a list of resources return the same list, assigning a | |
388 | unique id to each resource." | |
389 | (cond | |
390 | ((null resources) nil) | |
391 | (t | |
392 | (let* ((resource (car resources)) | |
393 | (unique-id (org-taskjuggler-get-unique-id resource unique-ids))) | |
394 | (push (cons "unique-id" unique-id) resource) | |
395 | (cons resource | |
396 | (org-taskjuggler-assign-resource-ids (cdr resources) | |
397 | (cons unique-id unique-ids))))))) | |
398 | ||
399 | (defun org-taskjuggler-resolve-dependencies (tasks) | |
400 | (let ((previous-level 0) | |
401 | siblings | |
402 | task resolved-tasks) | |
403 | (dolist (task tasks resolved-tasks) | |
404 | (let* ((level (cdr (assoc "level" task))) | |
405 | (depends (cdr (assoc "depends" task))) | |
406 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
407 | (blocker (cdr (assoc "BLOCKER" task))) | |
408 | (blocked-on-previous | |
409 | (and blocker (string-match "previous-sibling" blocker))) | |
410 | (dependencies | |
411 | (org-taskjuggler-resolve-explicit-dependencies | |
412 | (append | |
413 | (and depends (org-taskjuggler-tokenize-dependencies depends)) | |
414 | (and blocker (org-taskjuggler-tokenize-dependencies blocker))) | |
415 | tasks)) | |
416 | previous-sibling) | |
417 | ; update previous sibling info | |
418 | (cond | |
419 | ((< previous-level level) | |
420 | (dotimes (tmp (- level previous-level)) | |
421 | (push task siblings))) | |
422 | ((= previous-level level) | |
423 | (setq previous-sibling (car siblings)) | |
424 | (setcar siblings task)) | |
425 | ((> previous-level level) | |
426 | (dotimes (tmp (- previous-level level)) | |
427 | (pop siblings)) | |
428 | (setq previous-sibling (car siblings)) | |
429 | (setcar siblings task))) | |
430 | ; insert a dependency on previous sibling if the parent is | |
431 | ; ordered or if the tasks has a BLOCKER attribute with value "previous-sibling" | |
432 | (when (or (and previous-sibling parent-ordered) blocked-on-previous) | |
433 | (push (format "!%s" (cdr (assoc "unique-id" previous-sibling))) dependencies)) | |
434 | ; store dependency information | |
435 | (when dependencies | |
436 | (push (cons "depends" (mapconcat 'identity dependencies ", ")) task)) | |
437 | (setq previous-level level) | |
438 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
439 | ||
440 | (defun org-taskjuggler-tokenize-dependencies (dependencies) | |
441 | "Split a dependency property value DEPENDENCIES into the | |
442 | individual dependencies and return them as a list while keeping | |
443 | the optional arguments (such as gapduration) for the | |
444 | dependencies. A dependency will have to match `[-a-zA-Z0-9_]+'." | |
445 | (cond | |
446 | ((string-match "^ *$" dependencies) nil) | |
447 | ((string-match "^[ \t]*\\([-a-zA-Z0-9_]+\\([ \t]*{[^}]+}\\)?\\)[ \t,]*" dependencies) | |
448 | (cons | |
449 | (substring dependencies (match-beginning 1) (match-end 1)) | |
450 | (org-taskjuggler-tokenize-dependencies (substring dependencies (match-end 0))))) | |
451 | (t (error (format "invalid dependency id %s" dependencies))))) | |
452 | ||
453 | (defun org-taskjuggler-resolve-explicit-dependencies (dependencies tasks) | |
454 | "For each dependency in DEPENDENCIES try to find a | |
455 | corresponding task with a matching property \"task_id\" in TASKS. | |
456 | Return a list containing the resolved links for all DEPENDENCIES | |
457 | where a matching tasks was found. If the dependency is | |
458 | \"previous-sibling\" it is ignored (as this is dealt with in | |
459 | `org-taskjuggler-resolve-dependencies'). If there is no matching | |
460 | task the dependency is ignored and a warning is displayed ." | |
461 | (unless (null dependencies) | |
462 | (let* | |
463 | ;; the dependency might have optional attributes such as "{ | |
464 | ;; gapduration 5d }", so only use the first string as id for the | |
465 | ;; dependency | |
466 | ((dependency (car dependencies)) | |
467 | (id (car (split-string dependency))) | |
468 | (optional-attributes | |
469 | (mapconcat 'identity (cdr (split-string dependency)) " ")) | |
470 | (path (org-taskjuggler-find-task-with-id id tasks))) | |
471 | (cond | |
472 | ;; ignore previous sibling dependencies | |
473 | ((equal (car dependencies) "previous-sibling") | |
474 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)) | |
475 | ;; if the id is found in another task use its path | |
476 | ((not (null path)) | |
477 | (cons (mapconcat 'identity (list path optional-attributes) " ") | |
478 | (org-taskjuggler-resolve-explicit-dependencies | |
479 | (cdr dependencies) tasks))) | |
480 | ;; warn about dangling dependency but otherwise ignore it | |
481 | (t (display-warning | |
482 | 'org-export-taskjuggler | |
483 | (format "No task with matching property \"task_id\" found for id %s" id)) | |
484 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)))))) | |
485 | ||
486 | (defun org-taskjuggler-find-task-with-id (id tasks) | |
487 | "Find ID in tasks. If found return the path of task. Otherwise | |
488 | return nil." | |
489 | (let ((task-id (cdr (assoc "task_id" (car tasks)))) | |
490 | (path (cdr (assoc "path" (car tasks))))) | |
491 | (cond | |
492 | ((null tasks) nil) | |
493 | ((equal task-id id) path) | |
494 | (t (org-taskjuggler-find-task-with-id id (cdr tasks)))))) | |
495 | ||
496 | (defun org-taskjuggler-get-unique-id (item unique-ids) | |
497 | "Return a unique id for an ITEM which can be a task or a resource. | |
498 | The id is derived from the headline and made unique against | |
499 | UNIQUE-IDS. If the (downcased) first token of the headline is not | |
500 | unique try to add more (downcased) tokens of the headline or | |
501 | finally add more underscore characters (\"_\")." | |
502 | (let* ((headline (cdr (assoc "headline" item))) | |
503 | (parts (split-string headline)) | |
504 | (id (org-taskjuggler-clean-id (downcase (pop parts))))) | |
505 | ; try to add more parts of the headline to make it unique | |
506 | (while (member id unique-ids) | |
507 | (setq id (concat id "_" (org-taskjuggler-clean-id (downcase (pop parts)))))) | |
508 | ; if its still not unique add "_" | |
509 | (while (member id unique-ids) | |
510 | (setq id (concat id "_"))) | |
511 | id)) | |
512 | ||
513 | (defun org-taskjuggler-clean-id (id) | |
514 | "Clean and return ID to make it acceptable for taskjuggler." | |
515 | (and id (replace-regexp-in-string "[^a-zA-Z0-9_]" "_" id))) | |
516 | ||
517 | (defun org-taskjuggler-open-project (project) | |
518 | "Insert the beginning of a project declaration. All valid | |
519 | attributes from the PROJECT alist are inserted. If no end date is | |
520 | specified it is calculated | |
521 | `org-export-taskjuggler-default-project-duration' days from now." | |
522 | (let* ((unique-id (cdr (assoc "unique-id" project))) | |
523 | (headline (cdr (assoc "headline" project))) | |
524 | (version (cdr (assoc "version" project))) | |
525 | (start (cdr (assoc "start" project))) | |
526 | (end (cdr (assoc "end" project)))) | |
527 | (insert | |
528 | (format "project %s \"%s\" \"%s\" %s +%sd {\n }\n" | |
529 | unique-id headline version start | |
530 | org-export-taskjuggler-default-project-duration)))) | |
531 | ||
532 | (defun org-taskjuggler-filter-and-join (items) | |
533 | "Filter all nil elements from ITEMS and join the remaining ones | |
534 | with separator \"\n\"." | |
535 | (let ((filtered-items (remq nil items))) | |
536 | (and filtered-items (mapconcat 'identity filtered-items "\n")))) | |
537 | ||
538 | (defun org-taskjuggler-get-attributes (item attributes) | |
539 | "Return all attribute as a single formated string. ITEM is an | |
540 | alist representing either a resource or a task. ATTRIBUTES is a | |
541 | list of symbols. Only entries from ITEM are considered that are | |
542 | listed in ATTRIBUTES." | |
543 | (org-taskjuggler-filter-and-join | |
544 | (mapcar | |
545 | (lambda (attribute) | |
546 | (org-taskjuggler-filter-and-join | |
547 | (org-taskjuggler-get-attribute item attribute))) | |
548 | attributes))) | |
549 | ||
550 | (defun org-taskjuggler-get-attribute (item attribute) | |
551 | "Return a list of strings containing the properly formatted | |
552 | taskjuggler declaration for a given ATTRIBUTE in ITEM (an alist). | |
553 | If the ATTRIBUTE is not in ITEM return nil." | |
554 | (cond | |
555 | ((null item) nil) | |
556 | ((equal (symbol-name attribute) (car (car item))) | |
557 | (cons (format "%s %s" (symbol-name attribute) (cdr (car item))) | |
558 | (org-taskjuggler-get-attribute (cdr item) attribute))) | |
559 | (t (org-taskjuggler-get-attribute (cdr item) attribute)))) | |
560 | ||
561 | (defun org-taskjuggler-open-resource (resource) | |
562 | "Insert the beginning of a resource declaration. All valid | |
563 | attributes from the RESOURCE alist are inserted. If the RESOURCE | |
564 | defines a property \"resource_id\" it will be used as the id for | |
565 | this resource. Otherwise it will use the ID property. If neither | |
566 | is defined it will calculate a unique id for the resource using | |
567 | `org-taskjuggler-get-unique-id'." | |
568 | (let ((id (org-taskjuggler-clean-id | |
569 | (or (cdr (assoc "resource_id" resource)) | |
570 | (cdr (assoc "ID" resource)) | |
571 | (cdr (assoc "unique-id" resource))))) | |
572 | (headline (cdr (assoc "headline" resource))) | |
573 | (attributes '(limits vacation shift booking efficiency journalentry rate))) | |
574 | (insert | |
575 | (concat | |
576 | "resource " id " \"" headline "\" {\n " | |
577 | (org-taskjuggler-get-attributes resource attributes) "\n")))) | |
578 | ||
579 | (defun org-taskjuggler-clean-effort (effort) | |
580 | "Translate effort strings into a format acceptable to taskjuggler, | |
581 | i.e. REAL UNIT. If the effort string is something like 5:30 it | |
582 | will be assumed to be hours and will be translated into 5.5h. | |
583 | Otherwise if it contains something like 3.0 it is assumed to be | |
584 | days and will be translated into 3.0d. Other formats that | |
585 | taskjuggler supports (like weeks, months and years) are currently | |
586 | not supported." | |
587 | (cond | |
588 | ((null effort) effort) | |
589 | ((string-match "\\([0-9]+\\):\\([0-9]+\\)" effort) | |
590 | (let ((hours (string-to-number (match-string 1 effort))) | |
591 | (minutes (string-to-number (match-string 2 effort)))) | |
592 | (format "%dh" (+ hours (/ minutes 60.0))))) | |
593 | ((string-match "\\([0-9]+\\).\\([0-9]+\\)" effort) (concat effort "d")) | |
594 | (t (error "Not a valid effort (%s)" effort)))) | |
595 | ||
596 | (defun org-taskjuggler-get-priority (priority) | |
597 | "Return a priority between 1 and 1000 based on PRIORITY, an | |
598 | org-mode priority string." | |
599 | (max 1 (/ (* 1000 (- org-lowest-priority (string-to-char priority))) | |
600 | (- org-lowest-priority org-highest-priority)))) | |
601 | ||
602 | (defun org-taskjuggler-open-task (task) | |
603 | (let* ((unique-id (cdr (assoc "unique-id" task))) | |
604 | (headline (cdr (assoc "headline" task))) | |
605 | (effort (org-taskjuggler-clean-effort (cdr (assoc org-effort-property task)))) | |
606 | (depends (cdr (assoc "depends" task))) | |
607 | (allocate (cdr (assoc "allocate" task))) | |
608 | (priority-raw (cdr (assoc "PRIORITY" task))) | |
609 | (priority (and priority-raw (org-taskjuggler-get-priority priority-raw))) | |
610 | (state (cdr (assoc "TODO" task))) | |
611 | (complete (or (and (member state org-done-keywords) "100") | |
612 | (cdr (assoc "complete" task)))) | |
613 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
614 | (previous-sibling (cdr (assoc "previous-sibling" task))) | |
615 | (attributes | |
616 | '(account start note duration endbuffer endcredit end | |
617 | flags journalentry length maxend maxstart milestone | |
618 | minend minstart period reference responsible | |
619 | scheduling startbuffer startcredit statusnote))) | |
620 | (insert | |
621 | (concat | |
622 | "task " unique-id " \"" headline "\" {\n" | |
623 | (if (and parent-ordered previous-sibling) | |
624 | (format " depends %s\n" previous-sibling) | |
625 | (and depends (format " depends %s\n" depends))) | |
626 | (and allocate (format " purge allocations\n allocate %s\n" allocate)) | |
627 | (and complete (format " complete %s\n" complete)) | |
628 | (and effort (format " effort %s\n" effort)) | |
629 | (and priority (format " priority %s\n" priority)) | |
630 | ||
631 | (org-taskjuggler-get-attributes task attributes) | |
632 | "\n")))) | |
633 | ||
634 | (defun org-taskjuggler-close-maybe (level) | |
635 | (while (> org-export-taskjuggler-old-level level) | |
636 | (insert "}\n") | |
637 | (setq org-export-taskjuggler-old-level (1- org-export-taskjuggler-old-level))) | |
638 | (when (= org-export-taskjuggler-old-level level) | |
639 | (insert "}\n"))) | |
640 | ||
641 | (defun org-taskjuggler-insert-reports () | |
642 | (let (report) | |
643 | (dolist (report org-export-taskjuggler-default-reports) | |
644 | (insert report "\n")))) | |
645 | ||
646 | (provide 'org-taskjuggler) | |
647 | ||
648 | ;;; org-taskjuggler.el ends here |