Browse Source

ox-taskjuggler: Port TaskJuggler back-end to new export framework

* contrib/lisp/ox-taskjuggler.el: New file.
* lisp/org.el (org-export-backends): Add new back-end in customize
  interface.
Nicolas Goaziou 4 years ago
parent
commit
af9191d18b
2 changed files with 857 additions and 1 deletions
  1. 855 0
      contrib/lisp/ox-taskjuggler.el
  2. 2 1
      lisp/org.el

+ 855 - 0
contrib/lisp/ox-taskjuggler.el

@@ -0,0 +1,855 @@
+;;; ox-taskjuggler.el --- TaskJuggler Back-End for Org Export Engine
+;;
+;; Copyright (C) 2007-2013 Free Software Foundation, Inc.
+;;
+;; Emacs Lisp Archive Entry
+;; Filename: ox-taskjuggler.el
+;; Author: Christian Egli
+;;      Nicolas Goaziou <n dot goaziou at gmail dot com>
+;; Maintainer: Christian Egli
+;; Keywords: org, taskjuggler, project planning
+;; Description: Converts an Org mode buffer into a TaskJuggler project plan
+
+;; This program 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 of the License, 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;; Commentary:
+;;
+;; This library implements a TaskJuggler exporter for Org mode.
+;; TaskJuggler uses a text format to define projects, tasks and
+;; resources, so it is a natural fit for Org mode.  It can produce all
+;; sorts of reports for tasks or resources in either HTML, CSV or PDF.
+;; The current version of TaskJuggler requires KDE but the next
+;; version is implemented in Ruby and should therefore run on any
+;; platform.
+;;
+;; The exporter does not export all the nodes of a document or
+;; strictly follow the order of the nodes in the document.
+;;
+;; Instead the TaskJuggler exporter looks for a tree that defines the
+;; tasks and a optionally tree that defines the resources for this
+;; project.  It then creates a TaskJuggler file based on these trees
+;; and the attributes defined in all the nodes.
+;;
+;; * Installation
+;;
+;; Put this file into your load-path and the following line into your
+;; ~/.emacs:
+;;
+;;   (add-to-list 'org-export-backends 'taskjuggler)
+;;
+;; or customize `org-export-backends' variable.
+;;
+;; The interactive functions are the following:
+;;
+;; M-x `org-taskjuggler-export'
+;; M-x `org-taskjuggler-export-and-open'
+;;
+;; * Tasks
+;;
+;; Let's illustrate the usage with a small example.  Create your tasks
+;; as you usually do with org-mode.  Assign efforts to each task using
+;; properties (it's easiest to do this in the column view).  You
+;; should end up with something similar to the example by Peter Jones
+;; in:
+;;
+;;   http://www.contextualdevelopment.com/static/artifacts/articles/2008/project-planning/project-planning.org.
+;;
+;; Now mark the top node of your tasks with a tag named
+;; "taskjuggler_project" (or whatever you customized
+;; `org-taskjuggler-project-tag' to).  You are now ready to export the
+;; project plan with `org-taskjuggler-export-and-open' which will
+;; export the project plan and open a Gantt chart in TaskJugglerUI.
+;;
+;; * Resources
+;;
+;; Next you can define resources and assign those to work on specific
+;; tasks.  You can group your resources hierarchically.  Tag the top
+;; node of the resources with "taskjuggler_resource" (or whatever you
+;; customized `org-taskjuggler-resource-tag' to).  You can optionally
+;; assign an identifier (named "resource_id") to the resources (using
+;; the standard org properties commands) or you can let the exporter
+;; generate identifiers automatically (the exporter picks the first
+;; word of the headline as the identifier as long as it is unique, see
+;; the documentation of `org-taskjuggler--build-unique-id').  Using that
+;; identifier you can then allocate resources to tasks.  This is again
+;; done with the "allocate" property on the tasks.  Do this in column
+;; view or when on the task type
+;;
+;;  C-c C-x p allocate RET <resource_id> RET
+;;
+;; Once the allocations are done you can again export to TaskJuggler
+;; and check in the Resource Allocation Graph which person is working
+;; on what task at what time.
+;;
+;; * Export of properties
+;;
+;; The exporter also takes TODO state information into consideration,
+;; i.e. if a task is marked as done it will have the corresponding
+;; attribute in TaskJuggler ("complete 100").  Also it will export any
+;; property on a task resource or resource node which is known to
+;; TaskJuggler, such as limits, vacation, shift, booking, efficiency,
+;; journalentry, rate for resources or account, start, note, duration,
+;; end, journalentry, milestone, reference, responsible, scheduling,
+;; etc for tasks.
+;;
+;; * Dependencies
+;;
+;; The exporter will handle dependencies that are defined in the tasks
+;; either with the ORDERED attribute (see TODO dependencies in the Org
+;; mode manual) or with the BLOCKER attribute (see org-depend.el) or
+;; alternatively with a depends attribute.  Both the BLOCKER and the
+;; depends attribute can be either "previous-sibling" or a reference
+;; to an identifier (named "task_id") which is defined for another
+;; task in the project.  BLOCKER and the depends attribute can define
+;; multiple dependencies separated by either space or comma.  You can
+;; also specify optional attributes on the dependency by simply
+;; appending it.  The following examples should illustrate this:
+;;
+;; * Training material
+;;   :PROPERTIES:
+;;   :task_id:  training_material
+;;   :ORDERED:  t
+;;   :END:
+;; ** Markup Guidelines
+;;    :PROPERTIES:
+;;    :Effort:   2d
+;;    :END:
+;; ** Workflow Guidelines
+;;    :PROPERTIES:
+;;    :Effort:   2d
+;;    :END:
+;; * Presentation
+;;   :PROPERTIES:
+;;   :Effort:   2d
+;;   :BLOCKER:  training_material { gapduration 1d } some_other_task
+;;   :END:
+;;
+;;;; * TODO
+;;   - Look at org-file-properties, org-global-properties and
+;;     org-global-properties-fixed
+;;   - What about property inheritance and org-property-inherit-p?
+;;   - Use TYPE_TODO as an way to assign resources
+;;
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+(require 'ox)
+
+
+
+;;; User Variables
+
+(defgroup org-export-taskjuggler nil
+  "Options specific for TaskJuggler export back-end."
+  :tag "Org Export TaskJuggler"
+  :group 'org-export)
+
+(defcustom org-taskjuggler-extension ".tjp"
+  "Extension of TaskJuggler files."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'string)
+
+(defcustom org-taskjuggler-project-tag "taskjuggler_project"
+  "Tag marking project's tasks.
+This tag is used to find the tree containing all the tasks for
+the project."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'string)
+
+(defcustom org-taskjuggler-resource-tag "taskjuggler_resource"
+  "Tag marking project's resources.
+This tag is used to find the tree containing all the resources
+for the project."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'string)
+
+(defcustom org-taskjuggler-report-tag "taskjuggler_report"
+  "Tag marking project's reports.
+This tag is used to find the tree containing all the reports for
+the project."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'string)
+
+(defcustom org-taskjuggler-target-version 2.4
+  "Which version of TaskJuggler the exporter is targeting."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'number)
+
+(defcustom org-taskjuggler-default-project-version "1.0"
+  "Default version string for the project.
+This value can also be set with the \":VERSION:\" property
+associated to the headline defining the project."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'string)
+
+(defcustom org-taskjuggler-default-project-duration 280
+  "Default project duration.
+The value will be used if no start and end date have been defined
+in the root node of the task tree, i.e. the tree that has been
+marked with `org-taskjuggler-project-tag'"
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type 'integer)
+
+(defcustom org-taskjuggler-default-reports
+  '("taskreport \"Gantt Chart\" {
+  headline \"Project Gantt Chart\"
+  columns hierarchindex, name, start, end, effort, duration, completed, chart
+  timeformat \"%Y-%m-%d\"
+  hideresource 1
+  loadunit shortauto
+}"
+    "resourcereport \"Resource Graph\" {
+  headline \"Resource Allocation Graph\"
+  columns no, name, utilization, freeload, chart
+  loadunit shortauto
+  sorttasks startup
+  hidetask ~isleaf()
+}")
+  "Default reports for the project."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type '(repeat (string :tag "Report")))
+
+(defcustom org-taskjuggler-default-global-header ""
+  "Default global header for the project.
+This goes before project declaration, and might be useful for
+early macros."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type '(string :tag "Preamble"))
+
+(defcustom org-taskjuggler-default-global-properties
+  "shift s40 \"Part time shift\" {
+  workinghours wed, thu, fri off
+}
+"
+  "Default global properties for the project.
+
+Here you typically define global properties such as shifts,
+accounts, rates, vacation, macros and flags.  Any property that
+is allowed within the TaskJuggler file can be inserted.  You
+could for example include another TaskJuggler file.
+
+The global properties are inserted after the project declaration
+but before any resource and task declarations."
+  :group 'org-export-taskjuggler
+  :version "24.2"
+  :type '(string :tag "Preamble"))
+
+(defcustom org-taskjuggler-valid-task-attributes
+  '(account start note duration endbuffer endcredit end
+	    flags journalentry length limits maxend maxstart minend
+	    minstart period reference responsible scheduling
+	    startbuffer startcredit statusnote chargeset charge)
+  "Valid attributes for Taskjuggler tasks.
+If one of these appears as a property for a headline, it will be
+exported with the corresponding task."
+  :group 'org-export-taskjuggler)
+
+(defcustom org-taskjuggler-valid-resource-attributes
+  '(limits vacation shift booking efficiency journalentry rate
+	   workinghours flags)
+  "Valid attributes for Taskjuggler resources.
+If one of these appears as a property for a headline, it will be
+exported with the corresponding resource."
+  :group 'org-export-taskjuggler)
+
+(defcustom org-taskjuggler-valid-report-attributes
+  '(headline columns definitions timeformat hideresource hidetask
+	     loadunit sorttasks formats period)
+  "Valid attributes for Taskjuggler reports.
+If one of these appears as a property for a headline, it will be
+exported with the corresponding report."
+  :group 'org-export-taskjuggler)
+
+(defcustom org-taskjuggler-keep-project-as-task t
+  "Non-nil keeps the project headline as an umbrella task for all tasks.
+Setting this to nil will allow maintaining completely separated
+task buckets, while still sharing the same resources pool."
+  :group 'org-export-taskjuggler
+  :type 'boolean)
+
+
+
+;;; Hooks
+
+(defvar org-taskjuggler-final-hook nil
+  "Hook run after a TaskJuggler files has been saved.
+This hook is run with the name of the file as argument.")
+
+
+
+;;; Back-End Definition
+
+(org-export-define-backend taskjuggler
+  ((template . org-taskjuggler-project-plan))
+  :menu-entry
+  (?J "Export to TaskJuggler"
+      ((?j "As TJP file" (lambda (a s v b) (org-taskjuggler-export a s v)))
+       (?o "As TJP file and open"
+           (lambda (a s v b)
+             (if a (org-taskjuggler-export a s v)
+               (org-taskjuggler-export-and-open s v))))))
+  ;; This property will be used to store unique ids in communication
+  ;; channel.  Ids will be retrieved with `org-taskjuggler-get-id'.
+  :options-alist ((:taskjuggler-unique-ids nil nil nil)))
+
+
+
+;;; Unique IDs
+
+(defun org-taskjuggler-assign-task-ids (tasks info)
+  "Assign a unique ID to each task in TASKS.
+TASKS is a list of headlines.  Return value is an alist between
+headlines and their associated ID.  IDs are hierarchical, which
+means they only need to be unique among the task siblings."
+  (let* (alist
+         (build-id
+          (lambda (tasks local-ids)
+            (org-element-map tasks 'headline
+              (lambda (task)
+                (let ((id (org-taskjuggler--build-unique-id task local-ids)))
+                  (push id local-ids)
+                  (push (cons task id) alist)
+                  (funcall build-id (org-element-contents task) nil)))
+              info nil 'headline))))
+    (funcall build-id tasks nil)
+    alist))
+
+(defun org-taskjuggler-assign-resource-ids (resources info)
+  "Assign a unique ID to each resource within RESOURCES.
+RESOURCES is a list of headlines.  Return value is an alist
+between headlines and their associated ID."
+  (let (ids)
+    (org-element-map resources 'headline
+      (lambda (resource)
+        (let ((id (org-taskjuggler--build-unique-id resource ids)))
+          (push id ids)
+          (cons resource id)))
+      info)))
+
+
+
+;;; Accessors
+
+(defun org-taskjuggler-get-project (info)
+  "Return project in parse tree.
+INFO is a plist used as a communication channel.  First headline
+in buffer with `org-taskjuggler-project-tag' defines the project.
+If no such task is defined, pick the first headline in buffer.
+If there is no headline at all, return nil."
+  (or (org-element-map (plist-get info :parse-tree) 'headline
+        (lambda (hl)
+          (and (member org-taskjuggler-project-tag
+                       (org-export-get-tags hl info))
+               hl))
+        info t)
+      (org-element-map tree 'headline 'identity info t)))
+
+(defun org-taskjuggler-get-id (item info)
+  "Return id for task or resource ITEM.
+ITEM is a headline.  Return value is a string."
+  (cdr (assq item (plist-get info :taskjuggler-unique-ids))))
+
+(defun org-taskjuggler-get-name (item)
+  "Return name for task or resource ITEM.
+ITEM is a headline.  Return value is a string."
+  ;; Quote double quotes in name.
+  (replace-regexp-in-string
+   "\"" "\\\"" (org-element-property :raw-value item) t t))
+
+(defun org-taskjuggler-get-start (item)
+  "Return start date for task or resource ITEM.
+ITEM is a headline.  Return value is a string or nil if ITEM
+doesn't have any start date defined.."
+  (let ((scheduled (org-element-property :scheduled item)))
+    (and scheduled (org-timestamp-format scheduled "%Y-%02m-%02d"))))
+
+(defun org-taskjuggler-get-end (item)
+  "Return end date for task or resource ITEM.
+ITEM is a headline.  Return value is a string or nil if ITEM
+doesn't have any end date defined.."
+  (let ((deadline (org-element-property :deadline item)))
+    (and deadline (org-timestamp-format deadline "%Y-%02m-%02d"))))
+
+
+
+;;; Internal Functions
+
+(defun org-taskjuggler--indent-string (s)
+  "Indent string S by 2 spaces.
+Return new string.  If S is the empty string, return it."
+  (if (equal "" s) s (replace-regexp-in-string "^ *\\S-" "  \\&" s)))
+
+(defun org-taskjuggler--build-attributes (item attributes)
+  "Return attributes string for task, resource or report ITEM.
+ITEM is an headline.  ATTRIBUTES is a list of symbols
+representing valid attributes for ITEM."
+  (mapconcat
+   (lambda (attribute)
+     (let ((value (org-element-property
+                   (intern (upcase (format ":%s" attribute)))
+                   item)))
+       (and value (format "%s %s\n" attribute value))))
+   (remq nil attributes) ""))
+
+(defun org-taskjuggler--build-unique-id (item unique-ids)
+  "Return a unique id for a given task or a resource.
+ITEM is an `headline' type element representing the task or
+resource.  Its id is derived from its name and made unique
+against UNIQUE-IDS.  If the (downcased) first token of the
+headline is not unique try to add more (downcased) tokens of the
+headline or finally add more underscore characters (\"_\")."
+  (let* ((parts (org-split-string (org-element-property :raw-value item)))
+	 (id (org-taskjuggler--clean-id (downcase (pop parts)))))
+    ;; Try to add more parts of the headline to make it unique.
+    (while (and (car parts) (member id unique-ids))
+      (setq id (concat id "_"
+                       (org-taskjuggler--clean-id (downcase (pop parts))))))
+    ;; If it's still not unique, add "_".
+    (while (member id unique-ids)
+      (setq id (concat id "_")))
+    id))
+
+(defun org-taskjuggler--clean-id (id)
+  "Clean and return ID to make it acceptable for TaskJuggler.
+ID is a string."
+  ;; Replace non-ascii by "_".
+  (replace-regexp-in-string
+   "[^a-zA-Z0-9_]" "_"
+   ;; Make sure id doesn't start with a number.
+   (replace-regexp-in-string "^\\([0-9]\\)" "_\\1" id)))
+
+
+
+;;; Dependencies
+
+(defun org-taskjuggler-resolve-dependencies (task info)
+  "Return a list of all tasks TASK depends on.
+TASK is an headline.  INFO is a plist used as a communication
+channel."
+  (let ((deps-ids
+         ;; Get all dependencies specified in BLOCKER and DEPENDS task
+         ;; properties.  Clean options from them.
+         (let ((deps (concat (org-element-property :BLOCKER task)
+                             (org-element-property :DEPENDS task))))
+           (and deps
+                (org-split-string (replace-regexp-in-string "{.*?}" "" deps)
+                                  "[ ,]* +"))))
+        depends)
+    (when deps-ids
+      ;; Find tasks with :task_id: property matching id in DEPS-IDS.
+      ;; Add them to DEPENDS.
+      (let* ((project (org-taskjuggler-get-project info))
+             (tasks (if org-taskjuggler-keep-project-as-task project
+                      (org-element-contents project))))
+        (setq depends
+              (org-element-map tasks 'headline
+                (lambda (task)
+                  (let ((task-id (org-element-property :TASK_ID task)))
+                    (and task-id (member task-id deps-ids) task)))
+                info)))
+      ;; Check BLOCKER and DEPENDS properties.  If "previous-sibling"
+      ;; belongs to DEPS-ID, add it to DEPENDS.
+      (when (and (member-ignore-case "previous-sibling" deps-ids)
+                 (not (org-export-first-sibling-p task info)))
+        (let ((prev (org-export-get-previous-element task info)))
+          (and (not (memq prev depends)) (push prev depends)))))
+    ;; Check ORDERED status of parent.
+    (let ((parent (org-export-get-parent task)))
+      (when (and parent
+                 (org-element-property :ORDERED parent)
+                 (not (org-export-first-sibling-p task info)))
+        (push (org-export-get-previous-element task info) depends)))
+    ;; Return dependencies.
+    depends))
+
+(defun org-taskjuggler-format-dependencies (dependencies task info)
+  "Format DEPENDENCIES to match TaskJuggler syntax.
+DEPENDENCIES is list of dependencies for TASK, as returned by
+`org-taskjuggler-resolve-depedencies'.  TASK is an headline.
+INFO is a plist used as a communication channel.  Return value
+doesn't include leading \"depends\"."
+  (let ((dep-str (concat (org-element-property :BLOCKER task)
+                         " "
+                         (org-element-property :DEPENDS task)))
+        (get-path
+         (lambda (dep)
+           ;; Return path to DEP relatively to TASK.
+           (let ((parent (org-export-get-parent dep))
+                 (exclamations 1)
+                 (option
+                  (let ((id (org-element-property :TASK_ID dep)))
+                    (and id
+                         (string-match (concat id " +\\({.*?}\\)") dep-str)
+                         (org-match-string-no-properties 1))))
+                 path)
+             ;; Compute number of exclamation marks.
+             (while (not (org-element-map parent 'headline
+                           (lambda (task) (eq task dep))))
+               (incf exclamations)
+               (setq parent (org-export-get-parent parent)))
+             ;; Build path from DEP to PARENT.
+             (while (not (eq parent dep))
+               (push (org-taskjuggler-get-id dep info) path)
+               (setq dep (org-export-get-parent dep)))
+             ;; Return full path.  Add dependency options, if any.
+             (concat (make-string exclamations ?!)
+                     (mapconcat 'identity path ".")
+                     (and option (concat " " option)))))))
+    ;; Return dependencies string, without the leading "depends".
+    (mapconcat (lambda (dep) (funcall get-path dep)) dependencies ", ")))
+
+
+
+;;; Translator Functions
+
+(defun org-taskjuggler-project-plan (contents info)
+  "Build TaskJuggler project plan.
+CONTENTS is ignored. INFO is a plist holding export options.
+Return complete project plan as a string in TaskJuggler syntax."
+  (let* ((tree (plist-get info :parse-tree))
+         (project (or (org-taskjuggler-get-project info)
+                      (error "No project specified"))))
+    (concat
+     ;; 1. Insert header.
+     (org-element-normalize-string org-taskjuggler-default-global-header)
+     ;; 2. Insert project.
+     (org-taskjuggler--build-project project info)
+     ;; 3. Insert global properties.
+     (org-element-normalize-string org-taskjuggler-default-global-properties)
+     ;; 4. Insert resources.  Provide a default one if none is
+     ;;    specified.
+     (let ((main-resources
+            ;; Collect contents from various trees marked with
+            ;; `org-taskjuggler-resource-tag'.  Only gather top level
+            ;; resources.
+            (apply 'append
+                   (org-element-map tree 'headline
+                     (lambda (hl)
+                       (and (member org-taskjuggler-resource-tag
+                                    (org-export-get-tags hl info))
+                            (org-element-map (org-element-contents hl) 'headline
+                              'identity info nil 'headline)))
+                     info nil 'headline))))
+       ;; Assign a unique ID to each resource.  Store it under
+       ;; `:taskjuggler-unique-ids' property in INFO.
+       (setq info
+             (plist-put info :taskjuggler-unique-ids
+                        (org-taskjuggler-assign-resource-ids
+                         main-resources info)))
+       (concat
+        (if main-resources
+            (mapconcat
+             (lambda (resource) (org-taskjuggler--build-resource resource info))
+             main-resources "")
+          (format "resource %s \"%s\" {\n}\n" (user-login-name) user-full-name))
+        ;; 5. Insert tasks.
+        (let ((main-tasks
+               ;; If `org-taskjuggler-keep-project-as-task' is
+               ;; non-nil, there is only one task.  Otherwise, every
+               ;; direct children of PROJECT is a top level task.
+               (if org-taskjuggler-keep-project-as-task (list project)
+                 (or (org-element-map (org-element-contents project) 'headline
+                       'identity info nil 'headline)
+                     (error "No task specified")))))
+          ;; Assign a unique ID to each task.  Add it to
+          ;; `:taskjuggler-unique-ids' property in INFO.
+          (setq info
+                (plist-put info :taskjuggler-unique-ids
+                           (append
+                            (org-taskjuggler-assign-task-ids main-tasks info)
+                            (plist-get info :taskjuggler-unique-ids))))
+          ;; If no resource is allocated among tasks, allocate one to
+          ;; the first task.
+          (unless (org-element-map main-tasks 'headline
+                    (lambda (task) (org-element-property :ALLOCATE task))
+                    info t)
+            (org-element-put-property
+             (car main-tasks) :ALLOCATE
+             (or (org-taskjuggler-get-id (car main-resources) info)
+                 (user-login-name))))
+          (mapconcat
+           (lambda (task) (org-taskjuggler--build-task task info))
+           main-tasks ""))
+        ;; 6. Insert reports.  If no report is defined, insert default
+        ;;    reports.
+        (let ((main-reports
+               ;; Collect contents from various trees marked with
+               ;; `org-taskjuggler-report-tag'.  Only gather top level
+               ;; reports.
+               (apply 'append
+                      (org-element-map tree 'headline
+                        (lambda (hl)
+                          (and (member org-taskjuggler-report-tag
+                                       (org-export-get-tags hl info))
+                               (org-element-map (org-element-contents hl)
+                                   'headline 'identity info nil 'headline)))
+                        info nil 'headline))))
+          (if main-reports
+              (mapconcat
+               (lambda (report) (org-taskjuggler--build-report report info))
+               main-reports "")
+            (mapconcat 'org-element-normalize-string
+                       org-taskjuggler-default-reports ""))))))))
+
+(defun org-taskjuggler--build-project (project info)
+  "Return a project declaration.
+PROJECT is an headline.  INFO is a plist used as a communication
+channel.  If no start date is specified, start today.  If no end
+date is specified, end `org-taskjuggler-default-project-duration'
+days from now."
+  (format "project %s \"%s\" \"%s\" %s %s {\n}\n"
+          (org-taskjuggler-get-id project info)
+          (org-taskjuggler-get-name project)
+          ;; Version is obtained through :TASKJUGGLER_VERSION:
+          ;; property or `org-taskjuggler-default-project-version'.
+          (or (org-element-property :VERSION project)
+              org-taskjuggler-default-project-version)
+          (or (org-taskjuggler-get-start project)
+              (format-time-string "%Y-%m-%d"))
+          (let ((end (org-taskjuggler-get-end project)))
+            (or (and end (format "- %s" end))
+                (format "+%sd" org-taskjuggler-default-project-duration)))))
+
+(defun org-taskjuggler--build-resource (resource info)
+  "Return a resource declaration.
+
+RESOURCE is an headline.  INFO is a plist used as a communication
+channel.
+
+All valid attributes from RESOURCE are inserted.  If RESOURCE
+defines a property \"resource_id\" it will be used as the id for
+this resource.  Otherwise it will use the ID property.  If
+neither is defined a unique id will be associated to it."
+  (concat
+   ;; Opening resource.
+   (format "resource %s \"%s\" {\n"
+           (org-taskjuggler--clean-id
+            (or (org-element-property :RESOURCE_ID resource)
+                (org-element-property :ID resource)
+                (org-taskjuggler-get-id resource info)))
+           (org-taskjuggler-get-name resource))
+   ;; Add attributes.
+   (org-taskjuggler--indent-string
+    (org-taskjuggler--build-attributes
+     resource org-taskjuggler-valid-resource-attributes))
+   ;; Add inner resources.
+   (org-taskjuggler--indent-string
+    (mapconcat
+     'identity
+     (org-element-map (org-element-contents resource) 'headline
+       (lambda (hl) (org-taskjuggler--build-resource hl info))
+       info nil 'headline)
+     ""))
+   ;; Closing resource.
+   "}\n"))
+
+(defun org-taskjuggler--build-report (report)
+  "Return a report declaration.
+REPORT is an headline.  INFO is a plist used as a communication
+channel."
+  (concat
+   ;; Opening report.
+   (format "%s \"%s\" {\n"
+           (or (org-element-property :REPORT_KIND report) "taskreport")
+           (org-taskjuggler-get-name report))
+   ;; Add attributes.
+   (org-taskjuggler--indent-string
+    (org-taskjuggler--build-attributes
+     report org-taskjuggler-valid-report-attributes))
+   ;; Add inner reports.
+   (org-taskjuggler--indent-string
+    (mapconcat
+     'identity
+     (org-element-map (org-element-contents report) 'headline
+       (lambda (hl) (org-taskjuggler--build-report hl info))
+       info nil 'headline)
+     ""))
+   ;; Closing report.
+   "}\n"))
+
+(defun org-taskjuggler--build-task (task info)
+  "Return a task declaration.
+
+TASK is an headline.  INFO is a plist used as a communication
+channel.
+
+All valid attributes from TASK are inserted.  If TASK defines
+a property \"task_id\" it will be used as the id for this task.
+Otherwise it will use the ID property.  If neither is defined
+a unique id will be associated to it."
+  (let* ((allocate (org-element-property :ALLOCATE task))
+         (complete
+          (if (eq (org-element-property :todo-type task) 'done) "100"
+            (org-element-property :COMPLETE task)))
+         (depends (org-taskjuggler-resolve-dependencies task info))
+         (effort (org-element-property :EFFORT task))
+         (milestone
+          (or (org-element-property :MILESTONE task)
+              (and (org-element-map (org-element-contents task) 'headline
+                     'identity info t)  ; Has task any child?
+                   (not (or effort
+                            (org-element-property :LENGTH task)
+                            (org-element-property :DURATION task)
+                            (and (org-taskjuggler-get-start task)
+                                 (org-taskjuggler-get-end task))
+                            (org-element-property :PERIOD task))))))
+         (priority
+          (let ((pri (org-element-property :priority task)))
+            (and pri
+                 (max 1 (/ (* 1000 (- org-lowest-priority pri))
+                           (- org-lowest-priority org-highest-priority)))))))
+    (concat
+     ;; Opening task.
+     (format "task %s \"%s\" {\n"
+             (org-taskjuggler-get-id task info)
+             (org-taskjuggler-get-name task))
+     ;; Add default attributes.
+     (and depends
+          (format "  depends %s\n"
+                  (org-taskjuggler-format-dependencies depends task info)))
+     (and allocate
+          (format "  purge %s\n  allocate %s\n"
+                  ;; Compatibility for previous TaskJuggler versions.
+                  (if (>= org-export-taskjuggler-target-version 3.0) "allocate"
+                    "allocations")
+                  allocate))
+     (and complete (format "  complete %s\n" comptete))
+     (and effort
+          (format "  effort %s\n"
+                  (let* ((minutes (org-duration-string-to-minutes effort))
+                         (hours (/ minutes 60.0)))
+                    (format "%.1fh" hours))))
+     (and priority (format "  priority %s\n" complete))
+     (and milestone "  milestone\n")
+     ;; Add other valid attributes.
+     (org-taskjuggler--indent-string
+      (org-taskjuggler--build-attributes
+       task org-taskjuggler-valid-task-attributes))
+     ;; Add inner tasks.
+     (org-taskjuggler--indent-string
+      (mapconcat 'identity
+                 (org-element-map (org-element-contents task) 'headline
+                   (lambda (hl) (org-taskjuggler--build-task hl info))
+                   info nil 'headline)
+                 ""))
+     ;; Closing task.
+     "}\n")))
+
+
+
+;;; Interactive Functions
+
+;;;###autoload
+(defun org-taskjuggler-export (&optional async subtreep visible-only)
+  "Export current buffer to a TaskJuggler file.
+
+The exporter looks for a tree with tag that matches
+`org-taskjuggler-project-tag' and takes this as the tasks for
+this project.  The first node of this tree defines the project
+properties such as project name and project period.
+
+If there is a tree with tag that matches
+`org-taskjuggler-resource-tag' this tree is taken as resources
+for the project.  If no resources are specified, a default
+resource is created and allocated to the project.
+
+Also the TaskJuggler project will be created with default reports
+as defined in `org-taskjuggler-default-reports'.
+
+If narrowing is active in the current buffer, only export its
+narrowed part.
+
+If a region is active, export that region.
+
+A non-nil optional argument ASYNC means the process should happen
+asynchronously.  The resulting file should be accessible through
+the `org-export-stack' interface.
+
+When optional argument SUBTREEP is non-nil, export the sub-tree
+at point, extracting information from the headline properties
+first.
+
+When optional argument VISIBLE-ONLY is non-nil, don't export
+contents of hidden elements.
+
+Return output file's name."
+  (interactive)
+  (let ((outfile
+         (org-export-output-file-name org-taskjuggler-extension subtreep)))
+    (if async
+        (org-export-async-start
+            (lambda (f)
+              (org-export-add-to-stack f 'taskjuggler)
+              (run-hook-with-args 'org-taskjuggler-final-hook f))
+          `(expand-file-name
+            (org-export-to-file 'taskjuggler ,outfile ,subtreep ,visible-only)))
+      (org-export-to-file 'taskjuggler outfile subtreep visible-only)
+      (run-hook-with-args 'org-taskjuggler-final-hook outfile)
+      outfile)))
+
+;;;###autoload
+(defun org-taskjuggler-export-and-open (&optional subtreep visible-only)
+  "Export current buffer to a TaskJuggler file and open it.
+
+The exporter looks for a tree with tag that matches
+`org-taskjuggler-project-tag' and takes this as the tasks for
+this project.  The first node of this tree defines the project
+properties such as project name and project period.
+
+If there is a tree with tag that matches
+`org-taskjuggler-resource-tag' this tree is taken as resources
+for the project.  If no resources are specified, a default
+resource is created and allocated to the project.
+
+Also the TaskJuggler project will be created with default reports
+as defined in `org-taskjuggler-default-reports'.
+
+If narrowing is active in the current buffer, only export its
+narrowed part.
+
+If a region is active, export that region.
+
+When optional argument SUBTREEP is non-nil, export the sub-tree
+at point, extracting information from the headline properties
+first.
+
+When optional argument VISIBLE-ONLY is non-nil, don't export
+contents of hidden elements.
+
+Open file with the TaskJuggler GUI."
+  (interactive)
+  (let* ((file (org-taskjuggler-export nil subtreep visible-only))
+	 (process-name "TaskJugglerUI")
+	 (command (concat process-name " " file)))
+    (start-process-shell-command process-name nil command)))
+
+
+(provide 'ox-taskjuggler)
+
+;; Local variables:
+;; generated-autoload-file: "org-loaddefs.el"
+;; sentence-end-double-space: t
+;; End:
+
+;;; ox-taskjuggler.el ends here

+ 2 - 1
lisp/org.el

@@ -493,7 +493,8 @@ depends on, if any."
 	      (const :tag "   infojs:     Set up Sebastian Rose's JavaScript org-info.js" jsinfo)
 	      (const :tag "C  confluence  Export buffer to Confluence Wiki format" confluence)
 	      (const :tag "C  groff       Export buffer to Groff format" groff)
-	      (const :tag "C  koma-letter Export buffer to KOMA Scrlttrl2 format" koma-letter)))
+	      (const :tag "C  koma-letter Export buffer to KOMA Scrlttrl2 format" koma-letter)
+	      (const :tag "C  taskjuggler Export buffer to TaskJuggler format" taskjuggler)))
 
 (eval-after-load 'ox
   '(mapc