dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

ox-taskjuggler.el (39438B)


      1 ;;; ox-taskjuggler.el --- TaskJuggler Back-End for Org Export Engine
      2 ;;
      3 ;; Copyright (C) 2007-2021 Free Software Foundation, Inc.
      4 ;;
      5 ;; Emacs Lisp Archive Entry
      6 ;; Filename: ox-taskjuggler.el
      7 ;; Author: Christian Egli
      8 ;;      Nicolas Goaziou <n dot goaziou at gmail dot com>
      9 ;; Maintainer: Christian Egli
     10 ;; Keywords: org, taskjuggler, project planning
     11 ;; Description: Converts an Org mode buffer into a TaskJuggler project plan
     12 
     13 ;; This file is not part of GNU Emacs.
     14 
     15 ;; This program is free software: you can redistribute it and/or modify
     16 ;; it under the terms of the GNU General Public License as published by
     17 ;; the Free Software Foundation, either version 3 of the License, or
     18 ;; (at your option) any later version.
     19 
     20 ;; This program is distributed in the hope that it will be useful,
     21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     23 ;; GNU General Public License for more details.
     24 
     25 ;; You should have received a copy of the GNU General Public License
     26 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     27 
     28 ;;; Commentary:
     29 ;;
     30 ;; This library implements a TaskJuggler exporter for Org mode.
     31 ;; TaskJuggler is a project planing tool that uses a text format to
     32 ;; define projects, tasks and resources, so it is a natural fit for
     33 ;; Org mode.  It can produce all sorts of reports for tasks or
     34 ;; resources in either HTML, CSV or PDF.  TaskJuggler is implemented
     35 ;; in Ruby and should therefore run on any platform.
     36 ;;
     37 ;; The exporter does not export all the nodes of a document or
     38 ;; strictly follow the order of the nodes in the document.
     39 ;;
     40 ;; Instead the TaskJuggler exporter looks for a tree that defines the
     41 ;; tasks and a optionally tree that defines the resources for this
     42 ;; project.  It then creates a TaskJuggler file based on these trees
     43 ;; and the attributes defined in all the nodes.
     44 ;;
     45 ;; * Installation
     46 ;;
     47 ;; Put this file into your load-path and the following line into your
     48 ;; ~/.emacs:
     49 ;;
     50 ;;   (add-to-list 'org-export-backends 'taskjuggler)
     51 ;;
     52 ;; or customize `org-export-backends' variable.
     53 ;;
     54 ;; The interactive functions are the following:
     55 ;;
     56 ;; M-x `org-taskjuggler-export'
     57 ;; M-x `org-taskjuggler-export-and-open'
     58 ;;
     59 ;; * Tasks
     60 ;;
     61 ;; Let's illustrate the usage with a small example.  Create your tasks
     62 ;; as you usually do with org-mode.  Assign efforts to each task using
     63 ;; properties (it's easiest to do this in the column view).  You
     64 ;; should end up with something similar to the example by Peter Jones
     65 ;; in:
     66 ;;
     67 ;;   https://www.devalot.com/assets/articles/2008/07/project-planning/project-planning.org.
     68 ;;
     69 ;; Now mark the top node of your tasks with a tag named
     70 ;; "taskjuggler_project" (or whatever you customized
     71 ;; `org-taskjuggler-project-tag' to).  You are now ready to export the
     72 ;; project plan with `org-taskjuggler-export-and-open' which will
     73 ;; export the project plan and open a Gantt chart in TaskJugglerUI.
     74 ;;
     75 ;; * Resources
     76 ;;
     77 ;; Next you can define resources and assign those to work on specific
     78 ;; tasks.  You can group your resources hierarchically.  Tag the top
     79 ;; node of the resources with "taskjuggler_resource" (or whatever you
     80 ;; customized `org-taskjuggler-resource-tag' to).  You can optionally
     81 ;; assign an identifier (named "resource_id") to the resources (using
     82 ;; the standard org properties commands) or you can let the exporter
     83 ;; generate identifiers automatically (the exporter picks the first
     84 ;; word of the headline as the identifier as long as it is unique, see
     85 ;; the documentation of `org-taskjuggler--build-unique-id').  Using that
     86 ;; identifier you can then allocate resources to tasks.  This is again
     87 ;; done with the "allocate" property on the tasks.  Do this in column
     88 ;; view or when on the task type
     89 ;;
     90 ;;  C-c C-x p allocate RET <resource_id> RET
     91 ;;
     92 ;; Once the allocations are done you can again export to TaskJuggler
     93 ;; and check in the Resource Allocation Graph which person is working
     94 ;; on what task at what time.
     95 ;;
     96 ;; * Export of properties
     97 ;;
     98 ;; The exporter also takes TODO state information into consideration,
     99 ;; i.e. if a task is marked as done it will have the corresponding
    100 ;; attribute in TaskJuggler ("complete 100").  Also it will export any
    101 ;; property on a task resource or resource node which is known to
    102 ;; TaskJuggler, such as limits, vacation, shift, booking, efficiency,
    103 ;; journalentry, rate for resources or account, start, note, duration,
    104 ;; end, journalentry, milestone, reference, responsible, scheduling,
    105 ;; etc for tasks.
    106 ;;
    107 ;; * Dependencies
    108 ;;
    109 ;; The exporter will handle dependencies that are defined in the tasks
    110 ;; either with the ORDERED attribute (see TODO dependencies in the Org
    111 ;; mode manual) or with the BLOCKER attribute (see org-depend.el) or
    112 ;; alternatively with a depends attribute.  Both the BLOCKER and the
    113 ;; depends attribute can be either "previous-sibling" or a reference
    114 ;; to an identifier (named "task_id") which is defined for another
    115 ;; task in the project.  BLOCKER and the depends attribute can define
    116 ;; multiple dependencies separated by either space or comma.  You can
    117 ;; also specify optional attributes on the dependency by simply
    118 ;; appending it.  The following examples should illustrate this:
    119 ;;
    120 ;; * Training material
    121 ;;   :PROPERTIES:
    122 ;;   :task_id:  training_material
    123 ;;   :ORDERED:  t
    124 ;;   :END:
    125 ;; ** Markup Guidelines
    126 ;;    :PROPERTIES:
    127 ;;    :Effort:   2d
    128 ;;    :END:
    129 ;; ** Workflow Guidelines
    130 ;;    :PROPERTIES:
    131 ;;    :Effort:   2d
    132 ;;    :END:
    133 ;; * Presentation
    134 ;;   :PROPERTIES:
    135 ;;   :Effort:   2d
    136 ;;   :BLOCKER:  training_material { gapduration 1d } some_other_task
    137 ;;   :END:
    138 ;;
    139 ;;;; * TODO
    140 ;;   - Look at org-keyword-properties, org-global-properties and
    141 ;;     org-global-properties-fixed
    142 ;;   - What about property inheritance and org-property-inherit-p?
    143 ;;   - Use TYPE_TODO as an way to assign resources
    144 ;;   - Add support for org-export-with-planning
    145 ;;
    146 ;;; Code:
    147 
    148 (eval-when-compile (require 'cl))
    149 
    150 (require 'ox)
    151 
    152 
    153 
    154 ;;; User Variables
    155 
    156 (defgroup org-export-taskjuggler nil
    157   "Options specific for TaskJuggler export back-end."
    158   :tag "Org Export TaskJuggler"
    159   :group 'org-export)
    160 
    161 (defcustom org-taskjuggler-extension ".tjp"
    162   "Extension of TaskJuggler files."
    163   :group 'org-export-taskjuggler
    164   :type 'string)
    165 
    166 (defcustom org-taskjuggler-project-tag "taskjuggler_project"
    167   "Tag marking project's tasks.
    168 This tag is used to find the tree containing all the tasks for
    169 the project."
    170   :group 'org-export-taskjuggler
    171   :type 'string)
    172 
    173 (defcustom org-taskjuggler-resource-tag "taskjuggler_resource"
    174   "Tag marking project's resources.
    175 This tag is used to find the tree containing all the resources
    176 for the project."
    177   :group 'org-export-taskjuggler
    178   :type 'string)
    179 
    180 (defcustom org-taskjuggler-report-tag "taskjuggler_report"
    181   "Tag marking project's reports.
    182 This tag is used to find the tree containing all the reports for
    183 the project."
    184   :group 'org-export-taskjuggler
    185   :type 'string)
    186 
    187 (defcustom org-taskjuggler-target-version 3.0
    188   "Which version of TaskJuggler the exporter is targeting.
    189 By default a project plan is exported which conforms to version
    190 3.x of TaskJuggler.  For a project plan that is compatible with
    191 versions of TaskJuggler older than 3.0 set this to 2.4.
    192 
    193 If you change this variable be sure to also change
    194 `org-taskjuggler-default-reports' as the format of reports has
    195 changed considerably between version 2.x and 3.x of TaskJuggler"
    196   :group 'org-export-taskjuggler
    197   :type 'number)
    198 
    199 (defcustom org-taskjuggler-default-project-version "1.0"
    200   "Default version string for the project.
    201 This value can also be set with the \":VERSION:\" property
    202 associated to the headline defining the project."
    203   :group 'org-export-taskjuggler
    204   :type 'string)
    205 
    206 (defcustom org-taskjuggler-default-project-duration 280
    207   "Default project duration.
    208 The value will be used if no start and end date have been defined
    209 in the root node of the task tree, i.e. the tree that has been
    210 marked with `org-taskjuggler-project-tag'"
    211   :group 'org-export-taskjuggler
    212   :type 'integer)
    213 
    214 (defcustom org-taskjuggler-default-reports
    215   '("textreport report \"Plan\" {
    216   formats html
    217   header '== %title =='
    218 
    219   center -8<-
    220     [#Plan Plan] | [#Resource_Allocation Resource Allocation]
    221     ----
    222     === Plan ===
    223     <[report id=\"plan\"]>
    224     ----
    225     === Resource Allocation ===
    226     <[report id=\"resourceGraph\"]>
    227   ->8-
    228 }
    229 
    230 # A traditional Gantt chart with a project overview.
    231 taskreport plan \"\" {
    232   headline \"Project Plan\"
    233   columns bsi, name, start, end, effort, chart
    234   loadunit shortauto
    235   hideresource 1
    236 }
    237 
    238 # A graph showing resource allocation. It identifies whether each
    239 # resource is under- or over-allocated for.
    240 resourcereport resourceGraph \"\" {
    241   headline \"Resource Allocation Graph\"
    242   columns no, name, effort, weekly
    243   loadunit shortauto
    244   hidetask ~(isleaf() & isleaf_())
    245   sorttasks plan.start.up
    246 }")
    247   "Default reports for the project.
    248 These are sensible default reports to give a good out-of-the-box
    249 result when exporting without defining any reports.  \"%title\"
    250 anywhere in the reports will be replaced with the document title.
    251 If you want to define your own reports you can change them here
    252 or simply define the default reports so that they include an
    253 external report definition as follows:
    254 
    255 include reports.tji
    256 
    257 These default are made to work with tj3.  If you are targeting
    258 TaskJuggler 2.4 (see `org-taskjuggler-target-version') please
    259 change these defaults to something like the following:
    260 
    261 taskreport \"Gantt Chart\" {
    262   headline \"Project Gantt Chart\"
    263   columns hierarchindex, name, start, end, effort, duration, completed, chart
    264   timeformat \"%Y-%m-%d\"
    265   hideresource 1
    266   loadunit shortauto
    267 }
    268 
    269 resourcereport \"Resource Graph\" {
    270   headline \"Resource Allocation Graph\"
    271   columns no, name, utilization, freeload, chart
    272   loadunit shortauto
    273   sorttasks startup
    274   hidetask ~isleaf()
    275 }"
    276   :group 'org-export-taskjuggler
    277   :type '(repeat (string :tag "Report")))
    278 
    279 (defcustom org-taskjuggler-default-global-header ""
    280   "Default global header for the project.
    281 This goes before project declaration, and might be useful for
    282 early macros."
    283   :group 'org-export-taskjuggler
    284   :type '(string :tag "Preamble"))
    285 
    286 (defcustom org-taskjuggler-default-global-properties
    287   "shift s40 \"Part time shift\" {
    288   workinghours wed, thu, fri off
    289 }
    290 "
    291   "Default global properties for the project.
    292 
    293 Here you typically define global properties such as shifts,
    294 accounts, rates, vacation, macros and flags.  Any property that
    295 is allowed within the TaskJuggler file can be inserted.  You
    296 could for example include another TaskJuggler file.
    297 
    298 The global properties are inserted after the project declaration
    299 but before any resource and task declarations."
    300   :group 'org-export-taskjuggler
    301   :type '(string :tag "Preamble"))
    302 
    303 (defcustom org-taskjuggler-valid-task-attributes
    304   '(account start note duration endbuffer endcredit end
    305 	    flags journalentry length limits maxend maxstart minend
    306 	    minstart period reference responsible scheduling
    307 	    startbuffer startcredit statusnote chargeset charge)
    308   "Valid attributes for Taskjuggler tasks.
    309 If one of these appears as a property for a headline, it will be
    310 exported with the corresponding task.
    311 
    312 Note that multiline properties are not supported, so attributes
    313 like note or journalentry have to be on a single line."
    314   :group 'org-export-taskjuggler)
    315 
    316 (defcustom org-taskjuggler-valid-project-attributes
    317   '(timingresolution timezone alertlevels currency currencyformat
    318   dailyworkinghours extend includejournalentry now numberformat
    319   outputdir scenario shorttimeformat timeformat trackingscenario
    320   weekstartsmonday weekstartssunday workinghours
    321   yearlyworkingdays)
    322   "Valid attributes for Taskjuggler project.
    323 If one of these appears as a property for a headline that is a
    324 project definition, it will be exported with the corresponding
    325 task. Attribute 'timingresolution' should be the first in the
    326 list."
    327   :group 'org-export-taskjuggler)
    328 
    329 (defcustom org-taskjuggler-valid-resource-attributes
    330   '(limits vacation shift booking efficiency journalentry rate
    331 	   workinghours flags)
    332   "Valid attributes for Taskjuggler resources.
    333 If one of these appears as a property for a headline, it will be
    334 exported with the corresponding resource."
    335   :group 'org-export-taskjuggler)
    336 
    337 (defcustom org-taskjuggler-valid-report-attributes
    338   '(headline columns definitions timeformat hideresource hidetask
    339 	     loadunit sorttasks formats period)
    340   "Valid attributes for Taskjuggler reports.
    341 If one of these appears as a property for a headline, it will be
    342 exported with the corresponding report."
    343   :group 'org-export-taskjuggler)
    344 
    345 (defcustom org-taskjuggler-process-command
    346   "tj3 --silent --no-color --output-dir %o %f"
    347   "Command to process a Taskjuggler file.
    348 The command will be given to the shell as a command to process a
    349 Taskjuggler file.  \"%f\" in the command will be replaced by the
    350 full file name, \"%o\" by the reports directory (see
    351 `org-taskjuggler-reports-directory').
    352 
    353 If you are targeting Taskjuggler 2.4 (see
    354 `org-taskjuggler-target-version') this setting is ignored."
    355   :group 'org-export-taskjuggler)
    356 
    357 (defcustom org-taskjuggler-reports-directory "reports"
    358   "Default directory to generate the Taskjuggler reports in.
    359 The command `org-taskjuggler-process-command' generates the
    360 reports and associated files such as CSS inside this directory.
    361 
    362 If the directory is not an absolute path it is relative to the
    363 directory of the exported file.  The directory is created if it
    364 doesn't exist.
    365 
    366 If you are targeting Taskjuggler 2.4 (see
    367 `org-taskjuggler-target-version') this setting is ignored."
    368   :group 'org-export-taskjuggler)
    369 
    370 (defcustom org-taskjuggler-keep-project-as-task t
    371   "Non-nil keeps the project headline as an umbrella task for all tasks.
    372 Setting this to nil will allow maintaining completely separated
    373 task buckets, while still sharing the same resources pool."
    374   :group 'org-export-taskjuggler
    375   :type 'boolean)
    376 
    377 
    378 
    379 ;;; Hooks
    380 
    381 (defvar org-taskjuggler-final-hook nil
    382   "Hook run after a TaskJuggler files has been saved.
    383 This hook is run with the name of the file as argument.")
    384 
    385 
    386 
    387 ;;; Back-End Definition
    388 
    389 (org-export-define-backend 'taskjuggler
    390   '((template . org-taskjuggler-project-plan))
    391   :menu-entry
    392   '(?J "Export to TaskJuggler"
    393        ((?j "As TJP file" (lambda (a s v b) (org-taskjuggler-export a s v)))
    394 	(?p "As TJP file and process"
    395 	    (lambda (a s v b)
    396 	      (if a (org-taskjuggler-export a s v)
    397 		(org-taskjuggler-export-and-process s v))))
    398 	(?o "As TJP file, process and open"
    399 	    (lambda (a s v b)
    400 	      (if a (org-taskjuggler-export a s v)
    401 		(org-taskjuggler-export-process-and-open s v))))))
    402   ;; This property will be used to store unique ids in communication
    403   ;; channel.  Ids will be retrieved with `org-taskjuggler-get-id'.
    404   :options-alist '((:taskjuggler-unique-ids nil nil nil)))
    405 
    406 
    407 
    408 ;;; Unique IDs
    409 
    410 (defun org-taskjuggler-assign-task-ids (tasks info)
    411   "Assign a unique ID to each task in TASKS.
    412 TASKS is a list of headlines.  INFO is a plist used as a
    413 communication channel.  Return value is an alist between
    414 headlines and their associated ID.  IDs are hierarchical, which
    415 means they only need to be unique among the task siblings."
    416   (let* (alist
    417 	 build-id			; For byte-compiler.
    418          (build-id
    419           (lambda (tasks local-ids)
    420             (org-element-map tasks 'headline
    421               (lambda (task)
    422                 (let ((id (org-taskjuggler--build-unique-id task local-ids)))
    423                   (push id local-ids)
    424                   (push (cons task id) alist)
    425                   (funcall build-id (org-element-contents task) nil)))
    426               info nil 'headline))))
    427     (funcall build-id tasks nil)
    428     alist))
    429 
    430 (defun org-taskjuggler-assign-resource-ids (resources info)
    431   "Assign a unique ID to each resource within RESOURCES.
    432 RESOURCES is a list of headlines.  INFO is a plist used as a
    433 communication channel.  Return value is an alist between
    434 headlines and their associated ID."
    435   (let (ids)
    436     (org-element-map resources 'headline
    437       (lambda (resource)
    438         (let ((id (org-taskjuggler--build-unique-id resource ids)))
    439           (push id ids)
    440           (cons resource id)))
    441       info)))
    442 
    443 
    444 
    445 ;;; Accessors
    446 
    447 (defun org-taskjuggler-get-project (info)
    448   "Return project in parse tree.
    449 INFO is a plist used as a communication channel.  First headline
    450 in buffer with `org-taskjuggler-project-tag' defines the project.
    451 If no such task is defined, pick the first headline in buffer.
    452 If there is no headline at all, return nil."
    453   (let ((tree (plist-get info :parse-tree)))
    454     (or (org-element-map tree 'headline
    455 	  (lambda (hl)
    456 	    (and (member org-taskjuggler-project-tag
    457 			 (org-export-get-tags hl info))
    458 		 hl))
    459 	  info t)
    460 	(org-element-map tree 'headline 'identity info t))))
    461 
    462 (defun org-taskjuggler-get-id (item info)
    463   "Return id for task or resource ITEM.
    464 ITEM is a headline.  INFO is a plist used as a communication
    465 channel.  Return value is a string."
    466   (cdr (assq item (plist-get info :taskjuggler-unique-ids))))
    467 
    468 (defun org-taskjuggler-get-name (item)
    469   "Return name for task or resource ITEM.
    470 ITEM is a headline.  Return value is a string."
    471   ;; Quote double quotes in name.
    472   (replace-regexp-in-string
    473    "\"" "\\\"" (org-element-property :raw-value item) t t))
    474 
    475 (defun org-taskjuggler-get-start (item)
    476   "Return start date for task or resource ITEM.
    477 ITEM is a headline.  Return value is a string or nil if ITEM
    478 doesn't have any start date defined."
    479   (let ((scheduled (org-element-property :scheduled item)))
    480     (or
    481      (and scheduled (org-timestamp-format scheduled "%Y-%02m-%02d"))
    482      (and (memq 'start org-taskjuggler-valid-task-attributes)
    483 	  (org-element-property :START item)))))
    484 
    485 (defun org-taskjuggler-get-end (item)
    486   "Return end date for task or resource ITEM.
    487 ITEM is a headline.  Return value is a string or nil if ITEM
    488 doesn't have any end date defined."
    489   (let ((deadline (org-element-property :deadline item)))
    490     (and deadline (org-timestamp-format deadline "%Y-%02m-%02d"))))
    491 
    492 
    493 
    494 ;;; Internal Functions
    495 
    496 (defun org-taskjuggler--indent-string (s)
    497   "Indent string S by 2 spaces.
    498 Return new string.  If S is the empty string, return it."
    499   (if (equal "" s) s (replace-regexp-in-string "^ *\\S-" "  \\&" s)))
    500 
    501 (defun org-taskjuggler--build-attributes (item attributes)
    502   "Return attributes string for ITEM.
    503 ITEM is a project, task, resource or report headline.  ATTRIBUTES
    504 is a list of symbols representing valid attributes for ITEM."
    505   (mapconcat
    506    (lambda (attribute)
    507      (let ((value (org-element-property
    508                    (intern (upcase (format ":%s" attribute)))
    509                    item)))
    510        (and value (format "%s %s\n" attribute value))))
    511    (remq nil attributes) ""))
    512 
    513 (defun org-taskjuggler--build-unique-id (item unique-ids)
    514   "Return a unique id for a given task or a resource.
    515 ITEM is an `headline' type element representing the task or
    516 resource.  Its id is derived from its name and made unique
    517 against UNIQUE-IDS.  If the (downcased) first token of the
    518 headline is not unique try to add more (downcased) tokens of the
    519 headline or finally add more underscore characters (\"_\")."
    520   (let ((id (org-string-nw-p (org-element-property :TASK_ID item))))
    521     ;; If an id is specified, use it, as long as it's unique.
    522     (if (and id (not (member id unique-ids))) id
    523       (let* ((parts (split-string (org-element-property :raw-value item)))
    524 	     (id (org-taskjuggler--clean-id (downcase (pop parts)))))
    525 	;; Try to add more parts of the headline to make it unique.
    526 	(while (and (car parts) (member id unique-ids))
    527 	  (setq id (concat id "_"
    528 			   (org-taskjuggler--clean-id (downcase (pop parts))))))
    529 	;; If it's still not unique, add "_".
    530 	(while (member id unique-ids)
    531 	  (setq id (concat id "_")))
    532 	id))))
    533 
    534 (defun org-taskjuggler--clean-id (id)
    535   "Clean and return ID to make it acceptable for TaskJuggler.
    536 ID is a string."
    537   ;; Replace non-ascii by "_".
    538   (replace-regexp-in-string
    539    "[^a-zA-Z0-9_]" "_"
    540    ;; Make sure id doesn't start with a number.
    541    (replace-regexp-in-string "^\\([0-9]\\)" "_\\1" id)))
    542 
    543 
    544 
    545 ;;; Dependencies
    546 
    547 (defun org-taskjuggler-resolve-dependencies (task info)
    548   "Return a list of all tasks TASK depends on.
    549 TASK is a headline.  INFO is a plist used as a communication
    550 channel."
    551   (let ((deps-ids
    552          ;; Get all dependencies specified in BLOCKER and DEPENDS task
    553          ;; properties.  Clean options from them.
    554          (let ((deps (concat (org-element-property :BLOCKER task)
    555                              (org-element-property :DEPENDS task))))
    556            (and deps
    557                 (split-string (replace-regexp-in-string "{.*?}" "" deps)
    558 			      "[ ,]* +"))))
    559         depends)
    560     (when deps-ids
    561       ;; Find tasks with :task_id: property matching id in DEPS-IDS.
    562       ;; Add them to DEPENDS.
    563       (let* ((project (org-taskjuggler-get-project info))
    564              (tasks (if org-taskjuggler-keep-project-as-task project
    565                       (org-element-contents project))))
    566         (setq depends
    567               (org-element-map tasks 'headline
    568                 (lambda (task)
    569                   (let ((task-id (or (org-element-property :TASK_ID task)
    570 				     (org-element-property :ID task))))
    571                     (and task-id (member task-id deps-ids) task)))
    572                 info)))
    573       ;; Check BLOCKER and DEPENDS properties.  If "previous-sibling"
    574       ;; belongs to DEPS-ID, add it to DEPENDS.
    575       (when (and (member-ignore-case "previous-sibling" deps-ids)
    576                  (not (org-export-first-sibling-p task info)))
    577         (let ((prev (org-export-get-previous-element task info)))
    578           (and (not (memq prev depends)) (push prev depends)))))
    579     ;; Check ORDERED status of parent.
    580     (let ((parent (org-export-get-parent task)))
    581       (when (and parent
    582                  (org-element-property :ORDERED parent)
    583                  (not (org-export-first-sibling-p task info)))
    584         (push (org-export-get-previous-element task info) depends)))
    585     ;; Return dependencies.
    586     depends))
    587 
    588 (defun org-taskjuggler-format-dependencies (dependencies task info)
    589   "Format DEPENDENCIES to match TaskJuggler syntax.
    590 DEPENDENCIES is list of dependencies for TASK, as returned by
    591 `org-taskjuggler-resolve-depedencies'.  TASK is a headline.
    592 INFO is a plist used as a communication channel.  Return value
    593 doesn't include leading \"depends\"."
    594   (let* ((dep-str (concat (org-element-property :BLOCKER task)
    595 			  " "
    596 			  (org-element-property :DEPENDS task)))
    597 	 (get-path
    598 	  (lambda (dep)
    599 	    ;; Return path to DEP relatively to TASK.
    600 	    (let ((parent (org-export-get-parent task))
    601 		  (exclamations 1)
    602 		  (option
    603 		   (let ((id (org-element-property :TASK_ID dep)))
    604 		     (and id
    605 			  (string-match (concat id " +\\({.*?}\\)") dep-str)
    606 			  (match-string-no-properties 1 dep-str))))
    607 		  path)
    608 	      ;; Compute number of exclamation marks by looking for the
    609 	      ;; common ancestor between TASK and DEP.
    610 	      (while (not (org-element-map parent 'headline
    611 			  (lambda (hl) (eq hl dep))))
    612 		(cl-incf exclamations)
    613 		(setq parent (org-export-get-parent parent)))
    614 	      ;; Build path from DEP to PARENT.
    615 	      (while (not (eq parent dep))
    616 		(push (org-taskjuggler-get-id dep info) path)
    617 		(setq dep (org-export-get-parent dep)))
    618 	      ;; Return full path.  Add dependency options, if any.
    619 	      (concat (make-string exclamations ?!)
    620 		      (mapconcat 'identity path ".")
    621 		      (and option (concat " " option)))))))
    622     ;; Return dependencies string, without the leading "depends".
    623     (mapconcat (lambda (dep) (funcall get-path dep)) dependencies ", ")))
    624 
    625 
    626 
    627 ;;; Translator Functions
    628 
    629 (defun org-taskjuggler-project-plan (contents info)
    630   "Build TaskJuggler project plan.
    631 CONTENTS is ignored.  INFO is a plist holding export options.
    632 Return complete project plan as a string in TaskJuggler syntax."
    633   (let* ((tree (plist-get info :parse-tree))
    634          (project (or (org-taskjuggler-get-project info)
    635                       (error "No project specified"))))
    636     (concat
    637      ;; 1. Insert header.
    638      (org-element-normalize-string org-taskjuggler-default-global-header)
    639      ;; 2. Insert project.
    640      (org-taskjuggler--build-project project info)
    641      ;; 3. Insert global properties.
    642      (org-element-normalize-string org-taskjuggler-default-global-properties)
    643      ;; 4. Insert resources.  Provide a default one if none is
    644      ;;    specified.
    645      (let ((main-resources
    646             ;; Collect contents from various trees marked with
    647             ;; `org-taskjuggler-resource-tag'.  Only gather top level
    648             ;; resources.
    649             (apply 'append
    650                    (org-element-map tree 'headline
    651                      (lambda (hl)
    652                        (and (member org-taskjuggler-resource-tag
    653                                     (org-export-get-tags hl info))
    654                             (org-element-map (org-element-contents hl) 'headline
    655                               'identity info nil 'headline)))
    656                      info nil 'headline))))
    657        ;; Assign a unique ID to each resource.  Store it under
    658        ;; `:taskjuggler-unique-ids' property in INFO.
    659        (setq info
    660              (plist-put info :taskjuggler-unique-ids
    661                         (org-taskjuggler-assign-resource-ids
    662                          main-resources info)))
    663        (concat
    664         (if main-resources
    665             (mapconcat
    666              (lambda (resource) (org-taskjuggler--build-resource resource info))
    667              main-resources "")
    668           (format "resource %s \"%s\" {\n}\n" (user-login-name) user-full-name))
    669         ;; 5. Insert tasks.
    670         (let ((main-tasks
    671                ;; If `org-taskjuggler-keep-project-as-task' is
    672                ;; non-nil, there is only one task.  Otherwise, every
    673                ;; direct children of PROJECT is a top level task.
    674                (if org-taskjuggler-keep-project-as-task (list project)
    675                  (or (org-element-map (org-element-contents project) 'headline
    676                        'identity info nil 'headline)
    677                      (error "No task specified")))))
    678           ;; Assign a unique ID to each task.  Add it to
    679           ;; `:taskjuggler-unique-ids' property in INFO.
    680           (setq info
    681                 (plist-put info :taskjuggler-unique-ids
    682                            (append
    683                             (org-taskjuggler-assign-task-ids main-tasks info)
    684                             (plist-get info :taskjuggler-unique-ids))))
    685           ;; If no resource is allocated among tasks, allocate one to
    686           ;; the first task.
    687           (unless (org-element-map main-tasks 'headline
    688                     (lambda (task) (org-element-property :ALLOCATE task))
    689                     info t)
    690             (org-element-put-property
    691              (car main-tasks) :ALLOCATE
    692              (or (org-taskjuggler-get-id (car main-resources) info)
    693                  (user-login-name))))
    694           (mapconcat
    695            (lambda (task) (org-taskjuggler--build-task task info))
    696            main-tasks ""))
    697         ;; 6. Insert reports.  If no report is defined, insert default
    698         ;;    reports.
    699         (let ((main-reports
    700                ;; Collect contents from various trees marked with
    701                ;; `org-taskjuggler-report-tag'.  Only gather top level
    702                ;; reports.
    703                (apply 'append
    704                       (org-element-map tree 'headline
    705                         (lambda (hl)
    706                           (and (member org-taskjuggler-report-tag
    707                                        (org-export-get-tags hl info))
    708                                (org-element-map (org-element-contents hl)
    709                                    'headline 'identity info nil 'headline)))
    710                         info nil 'headline))))
    711           (if main-reports
    712               (mapconcat
    713                (lambda (report) (org-taskjuggler--build-report report info))
    714                main-reports "")
    715 	    ;; insert title in default reports
    716 	    (let* ((title (org-export-data (plist-get info :title) info))
    717 		   (report-title (if (string= title "")
    718 				     (org-taskjuggler-get-name project)
    719 				   title)))
    720 	      (mapconcat
    721 	       'org-element-normalize-string
    722 	       (mapcar
    723 		(lambda (report)
    724 		  (replace-regexp-in-string "%title" report-title  report t t))
    725 		org-taskjuggler-default-reports) "")))))))))
    726 
    727 (defun org-taskjuggler--build-project (project info)
    728   "Return a project declaration.
    729 PROJECT is a headline.  INFO is a plist used as a communication
    730 channel.  If no start date is specified, start today.  If no end
    731 date is specified, end `org-taskjuggler-default-project-duration'
    732 days from now."
    733   (concat
    734    ;; Opening project.
    735    (format "project %s \"%s\" \"%s\" %s %s {\n"
    736 	   (org-taskjuggler-get-id project info)
    737 	   (org-taskjuggler-get-name project)
    738 	   ;; Version is obtained through :TASKJUGGLER_VERSION:
    739 	   ;; property or `org-taskjuggler-default-project-version'.
    740 	   (or (org-element-property :VERSION project)
    741 	       org-taskjuggler-default-project-version)
    742 	   (or (org-taskjuggler-get-start project)
    743 	       (format-time-string "%Y-%m-%d"))
    744 	   (let ((end (org-taskjuggler-get-end project)))
    745 	     (or (and end (format "- %s" end))
    746 		 (format "+%sd"
    747 			 org-taskjuggler-default-project-duration))))
    748    ;; Add attributes.
    749    (org-taskjuggler--indent-string
    750     (org-taskjuggler--build-attributes
    751      project org-taskjuggler-valid-project-attributes))
    752    ;; Closing project.
    753    "}\n"))
    754 
    755 (defun org-taskjuggler--build-resource (resource info)
    756   "Return a resource declaration.
    757 
    758 RESOURCE is a headline.  INFO is a plist used as a communication
    759 channel.
    760 
    761 All valid attributes from RESOURCE are inserted.  If RESOURCE
    762 defines a property \"resource_id\" it will be used as the id for
    763 this resource.  Otherwise it will use the ID property.  If
    764 neither is defined a unique id will be associated to it."
    765   (concat
    766    ;; Opening resource.
    767    (format "resource %s \"%s\" {\n"
    768            (org-taskjuggler--clean-id
    769             (or (org-element-property :RESOURCE_ID resource)
    770                 (org-element-property :ID resource)
    771                 (org-taskjuggler-get-id resource info)))
    772            (org-taskjuggler-get-name resource))
    773    ;; Add attributes.
    774    (org-taskjuggler--indent-string
    775     (org-taskjuggler--build-attributes
    776      resource org-taskjuggler-valid-resource-attributes))
    777    ;; Add inner resources.
    778    (org-taskjuggler--indent-string
    779     (mapconcat
    780      'identity
    781      (org-element-map (org-element-contents resource) 'headline
    782        (lambda (hl) (org-taskjuggler--build-resource hl info))
    783        info nil 'headline)
    784      ""))
    785    ;; Closing resource.
    786    "}\n"))
    787 
    788 (defun org-taskjuggler--build-report (report info)
    789   "Return a report declaration.
    790 REPORT is a headline.  INFO is a plist used as a communication
    791 channel."
    792   (concat
    793    ;; Opening report.
    794    (format "%s \"%s\" {\n"
    795            (or (org-element-property :REPORT_KIND report) "taskreport")
    796            (org-taskjuggler-get-name report))
    797    ;; Add attributes.
    798    (org-taskjuggler--indent-string
    799     (org-taskjuggler--build-attributes
    800      report org-taskjuggler-valid-report-attributes))
    801    ;; Add inner reports.
    802    (org-taskjuggler--indent-string
    803     (mapconcat
    804      'identity
    805      (org-element-map (org-element-contents report) 'headline
    806        (lambda (hl) (org-taskjuggler--build-report hl info))
    807        info nil 'headline)
    808      ""))
    809    ;; Closing report.
    810    "}\n"))
    811 
    812 (defun org-taskjuggler--build-task (task info)
    813   "Return a task declaration.
    814 
    815 TASK is a headline.  INFO is a plist used as a communication
    816 channel.
    817 
    818 All valid attributes from TASK are inserted.  If TASK defines
    819 a property \"task_id\" it will be used as the id for this task.
    820 Otherwise it will use the ID property.  If neither is defined
    821 a unique id will be associated to it."
    822   (let* ((allocate (org-element-property :ALLOCATE task))
    823          (complete
    824           (if (eq (org-element-property :todo-type task) 'done) "100"
    825             (org-element-property :COMPLETE task)))
    826          (depends (org-taskjuggler-resolve-dependencies task info))
    827          (effort (let ((property
    828 			(intern (concat ":" (upcase org-effort-property)))))
    829 		   (org-element-property property task)))
    830          (milestone
    831           (or (org-element-property :MILESTONE task)
    832               (not (or (org-element-map (org-element-contents task) 'headline
    833 			 'identity info t)  ; Has task any child?
    834 		       effort
    835 		       (org-element-property :LENGTH task)
    836 		       (org-element-property :DURATION task)
    837 		       (and (org-taskjuggler-get-start task)
    838 			    (org-taskjuggler-get-end task))
    839 		       (org-element-property :PERIOD task)))))
    840          (priority
    841           (let ((pri (org-element-property :priority task)))
    842             (and pri
    843                  (max 1 (/ (* 1000 (- org-priority-lowest pri))
    844                            (- org-priority-lowest org-priority-highest)))))))
    845     (concat
    846      ;; Opening task.
    847      (format "task %s \"%s\" {\n"
    848              (org-taskjuggler-get-id task info)
    849              (org-taskjuggler-get-name task))
    850      ;; Add default attributes.
    851      (and depends
    852           (format "  depends %s\n"
    853                   (org-taskjuggler-format-dependencies depends task info)))
    854      (and allocate
    855           (format "  purge %s\n  allocate %s\n"
    856                   ;; Compatibility for previous TaskJuggler versions.
    857                   (if (>= org-taskjuggler-target-version 3.0) "allocate"
    858                     "allocations")
    859                   allocate))
    860      (and complete (format "  complete %s\n" complete))
    861      (and effort (format "  effort %s\n" effort))
    862      (and priority (format "  priority %s\n" priority))
    863      (and milestone "  milestone\n")
    864      ;; Add other valid attributes.
    865      (org-taskjuggler--indent-string
    866       (org-taskjuggler--build-attributes
    867        task org-taskjuggler-valid-task-attributes))
    868      ;; Add inner tasks.
    869      (org-taskjuggler--indent-string
    870       (mapconcat 'identity
    871                  (org-element-map (org-element-contents task) 'headline
    872                    (lambda (hl) (org-taskjuggler--build-task hl info))
    873                    info nil 'headline)
    874                  ""))
    875      ;; Closing task.
    876      "}\n")))
    877 
    878 
    879 
    880 ;;; Interactive Functions
    881 
    882 ;;;###autoload
    883 (defun org-taskjuggler-export (&optional async subtreep visible-only)
    884   "Export current buffer to a TaskJuggler file.
    885 
    886 The exporter looks for a tree with tag that matches
    887 `org-taskjuggler-project-tag' and takes this as the tasks for
    888 this project.  The first node of this tree defines the project
    889 properties such as project name and project period.
    890 
    891 If there is a tree with tag that matches
    892 `org-taskjuggler-resource-tag' this tree is taken as resources
    893 for the project.  If no resources are specified, a default
    894 resource is created and allocated to the project.
    895 
    896 Also the TaskJuggler project will be created with default reports
    897 as defined in `org-taskjuggler-default-reports'.
    898 
    899 If narrowing is active in the current buffer, only export its
    900 narrowed part.
    901 
    902 If a region is active, export that region.
    903 
    904 A non-nil optional argument ASYNC means the process should happen
    905 asynchronously.  The resulting file should be accessible through
    906 the `org-export-stack' interface.
    907 
    908 When optional argument SUBTREEP is non-nil, export the sub-tree
    909 at point, extracting information from the headline properties
    910 first.
    911 
    912 When optional argument VISIBLE-ONLY is non-nil, don't export
    913 contents of hidden elements.
    914 
    915 Return output file's name."
    916   (interactive)
    917   (let ((outfile
    918          (org-export-output-file-name org-taskjuggler-extension subtreep)))
    919     (org-export-to-file 'taskjuggler outfile
    920       async subtreep visible-only nil nil
    921       (lambda (file)
    922 	(run-hook-with-args 'org-taskjuggler-final-hook file) nil))))
    923 
    924 ;;;###autoload
    925 (defun org-taskjuggler-export-and-process (&optional subtreep visible-only)
    926   "Export current buffer to a TaskJuggler file and process it.
    927 
    928 The exporter looks for a tree with tag that matches
    929 `org-taskjuggler-project-tag' and takes this as the tasks for
    930 this project.  The first node of this tree defines the project
    931 properties such as project name and project period.
    932 
    933 If there is a tree with tag that matches
    934 `org-taskjuggler-resource-tag' this tree is taken as resources
    935 for the project.  If no resources are specified, a default
    936 resource is created and allocated to the project.
    937 
    938 Also the TaskJuggler project will be created with default reports
    939 as defined in `org-taskjuggler-default-reports'.
    940 
    941 If narrowing is active in the current buffer, only export its
    942 narrowed part.
    943 
    944 If a region is active, export that region.
    945 
    946 When optional argument SUBTREEP is non-nil, export the sub-tree
    947 at point, extracting information from the headline properties
    948 first.
    949 
    950 When optional argument VISIBLE-ONLY is non-nil, don't export
    951 contents of hidden elements.
    952 
    953 Return a list of reports."
    954   (interactive)
    955   (let ((file (org-taskjuggler-export nil subtreep visible-only)))
    956     (org-taskjuggler-compile file)))
    957 
    958 ;;;###autoload
    959 (defun org-taskjuggler-export-process-and-open (&optional subtreep visible-only)
    960   "Export current buffer to a TaskJuggler file, process and open it.
    961 
    962 Export and process the file using
    963 `org-taskjuggler-export-and-process' and open the generated
    964 reports with a browser.
    965 
    966 If you are targeting TaskJuggler 2.4 (see
    967 `org-taskjuggler-target-version') the processing and display of
    968 the reports is done using the TaskJuggler GUI."
    969   (interactive)
    970   (if (< org-taskjuggler-target-version 3.0)
    971       (let* ((process-name "TaskJugglerUI")
    972 	     (command
    973 	      (concat process-name " "
    974 		      (org-taskjuggler-export nil subtreep visible-only))))
    975 	(start-process-shell-command process-name nil command))
    976     (dolist (report (org-taskjuggler-export-and-process subtreep visible-only))
    977       (org-open-file report))))
    978 
    979 (defun org-taskjuggler-compile (file)
    980   "Compile a TaskJuggler file.
    981 
    982 FILE is the name of the file being compiled.  Processing is done
    983 through the command given in `org-taskjuggler-process-command'.
    984 
    985 Return a list of reports."
    986   (let* ((full-name (file-truename file))
    987 	 (out-dir
    988 	  (expand-file-name
    989 	   org-taskjuggler-reports-directory (file-name-directory file)))
    990 	 errors)
    991     (message (format "Processing TaskJuggler file %s..." file))
    992     (save-window-excursion
    993       (let ((outbuf (get-buffer-create "*Org Taskjuggler Output*")))
    994 	(unless (file-directory-p out-dir)
    995 	  (make-directory out-dir t))
    996 	(with-current-buffer outbuf (erase-buffer))
    997 	(shell-command
    998 	 (replace-regexp-in-string
    999 	  "%f" (shell-quote-argument full-name)
   1000 	  (replace-regexp-in-string
   1001 	   "%o" (shell-quote-argument out-dir)
   1002 	   org-taskjuggler-process-command t t) t t) outbuf)
   1003 	;; Collect standard errors from output buffer.
   1004 	(setq errors (org-taskjuggler--collect-errors outbuf)))
   1005       (if (not errors)
   1006 	  (message "Process completed.")
   1007 	(error (format "TaskJuggler failed with errors: %s" errors))))
   1008     (file-expand-wildcards (format "%s/*.html" out-dir))))
   1009 
   1010 (defun org-taskjuggler--collect-errors (buffer)
   1011   "Collect some kind of errors from \"tj3\" command output.
   1012 
   1013 BUFFER is the buffer containing output.
   1014 
   1015 Return collected error types as a string, or nil if there was
   1016 none."
   1017   (with-current-buffer buffer
   1018     (save-excursion
   1019       (goto-char (point-min))
   1020       (let ((case-fold-search t)
   1021 	    (errors ""))
   1022 	(while (re-search-forward "^.+:[0-9]+: \\(.*\\)$" nil t)
   1023 	  (setq errors (concat errors " " (match-string 1))))
   1024 	(and (org-string-nw-p errors) (org-trim errors))))))
   1025 
   1026 
   1027 (provide 'ox-taskjuggler)
   1028 
   1029 ;; Local variables:
   1030 ;; sentence-end-double-space: t
   1031 ;; End:
   1032 
   1033 ;;; ox-taskjuggler.el ends here