ox-icalendar: Add support for unscheduled and repeating TODOs
* lisp/ox-icalendar.el (org-icalendar-todo-unscheduled-start): New customization to control the exported start time of unscheduled tasks. (org-icalendar--rrule): Helper function for RRULE export. (org-icalendar--vevent): Use the new helper function for RRULE. (org-icalendar--repeater-type): Helper function to get the repeater type, and display warning if not supported. (org-icalendar--vtodo): Change how unscheduled TODOs are handled using the new customization option. Export SCHEDULED and DEADLINE repeaters. In case of SCHEDULED repeater and a DEADLINE without repeater, treat DEADLINE as RRULE UNTIL. Emit a warning for tricky edge cases that are not yet implemented. * testing/lisp/test-ox-icalendar.el (test-ox-icalendar/todo-repeater-shared): Test for exporting shared SCHEDULED/DEADLINE repeater. (test-ox-icalendar/todo-repeating-deadline-warndays): Test using warning days as DTSTART of repeating deadline. (test-ox-icalendar/todo-repeater-until): Test using DEADLINE as RRULE UNTIL. (test-ox-icalendar/todo-repeater-until-utc): Test RRULE UNTIL is in UTC format when DTSTART is not in local time format. (test-ox-icalendar/warn-unsupported-repeater): Unit test to warn for unsupported repeater types. * lisp/org-lint.el (org-lint-mismatched-planning-repeaters): Add lint for mismatched SCHEDULED and DEADLINE repeaters. * testing/lisp/test-org-lint.el (test-org-lint/mismatched-planning-repeaters): Add test for linting of mismatched SCHEDULED and DEADLINE repeaters. * doc/org-manual.org (iCalendar Export): Add link to new variable `org-icalendar-todo-unscheduled-start'.
This commit is contained in:
parent
d50956e480
commit
294a4d2fe2
|
@ -16071,14 +16071,16 @@ standard iCalendar format.
|
||||||
#+vindex: org-icalendar-include-todo
|
#+vindex: org-icalendar-include-todo
|
||||||
#+vindex: org-icalendar-use-deadline
|
#+vindex: org-icalendar-use-deadline
|
||||||
#+vindex: org-icalendar-use-scheduled
|
#+vindex: org-icalendar-use-scheduled
|
||||||
|
#+vindex: org-icalendar-todo-unscheduled-start
|
||||||
The iCalendar export backend can also incorporate TODO entries based
|
The iCalendar export backend can also incorporate TODO entries based
|
||||||
on the configuration of the ~org-icalendar-include-todo~ variable.
|
on the configuration of the ~org-icalendar-include-todo~ variable.
|
||||||
The backend exports plain timestamps as =VEVENT=, TODO items as
|
The backend exports plain timestamps as =VEVENT=, TODO items as
|
||||||
=VTODO=, and also create events from deadlines that are in non-TODO
|
=VTODO=, and also create events from deadlines that are in non-TODO
|
||||||
items. The backend uses the deadlines and scheduling dates in Org
|
items. The backend uses the deadlines and scheduling dates in Org
|
||||||
TODO items for setting the start and due dates for the iCalendar TODO
|
TODO items for setting the start and due dates for the iCalendar TODO
|
||||||
entry. Consult the ~org-icalendar-use-deadline~ and
|
entry. Consult the ~org-icalendar-use-deadline~,
|
||||||
~org-icalendar-use-scheduled~ variables for more details.
|
~org-icalendar-use-scheduled~, and
|
||||||
|
~org-icalendar-todo-unscheduled-start~ variables for more details.
|
||||||
|
|
||||||
#+vindex: org-icalendar-categories
|
#+vindex: org-icalendar-categories
|
||||||
#+vindex: org-icalendar-alarm-time
|
#+vindex: org-icalendar-alarm-time
|
||||||
|
|
64
etc/ORG-NEWS
64
etc/ORG-NEWS
|
@ -50,6 +50,21 @@ ox-icalendar. In particular, older versions of org-caldav may
|
||||||
encounter issues, and users are advised to update to the most recent
|
encounter issues, and users are advised to update to the most recent
|
||||||
version of org-caldav. See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
|
version of org-caldav. See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
|
||||||
|
|
||||||
|
*** Icalendar export of unscheduled TODOs no longer have start time of today
|
||||||
|
|
||||||
|
For TODOs without a scheduled start time, ox-icalendar no longer
|
||||||
|
forces them to have a scheduled start time of today when exporting.
|
||||||
|
|
||||||
|
Instead, the new customization ~org-icalendar-todo-unscheduled-start~
|
||||||
|
controls the exported start date for unscheduled tasks. Its default
|
||||||
|
is ~recurring-deadline-warning~ which will export unscheduled tasks
|
||||||
|
with no start date, unless it has a recurring deadline (in which case
|
||||||
|
the iCalendar spec demands a start date, and
|
||||||
|
~org-deadline-warning-days~ is used for that).
|
||||||
|
|
||||||
|
To revert to the old behavior, set
|
||||||
|
~org-icalendar-todo-unscheduled-start~ to ~current-datetime~.
|
||||||
|
|
||||||
** New and changed options
|
** New and changed options
|
||||||
*** Commands affected by ~org-fold-catch-invisible-edits~ can now be customized
|
*** Commands affected by ~org-fold-catch-invisible-edits~ can now be customized
|
||||||
|
|
||||||
|
@ -188,6 +203,28 @@ default settings of "Body only", "Visible only", and "Force
|
||||||
publishing" in the ~org-export-dispatch~ UI to be customized,
|
publishing" in the ~org-export-dispatch~ UI to be customized,
|
||||||
respectively.
|
respectively.
|
||||||
|
|
||||||
|
*** New option ~org-icalendar-todo-unscheduled-start~ to control unscheduled TODOs in ox-icalendar
|
||||||
|
|
||||||
|
~org-icalendar-todo-unscheduled-start~ controls how ox-icalendar
|
||||||
|
exports the starting datetime for unscheduled TODOs. Note this option
|
||||||
|
only has an effect when ~org-icalendar-include-todo~ is non-nil.
|
||||||
|
|
||||||
|
By default, ox-icalendar will not export a start datetime for
|
||||||
|
unscheduled TODOs, except in cases where the iCalendar spec demands a
|
||||||
|
start (specifically, for recurring deadlines, in which case
|
||||||
|
~org-deadline-warning-days~ is used).
|
||||||
|
|
||||||
|
Currently implemented options are:
|
||||||
|
|
||||||
|
- ~recurring-deadline-warning~: The default as described above.
|
||||||
|
- ~deadline-warning~: Use ~org-deadline-warning-days~ to set the start
|
||||||
|
time if the unscheduled task has a deadline (recurring or not).
|
||||||
|
- ~current-datetime~: Revert to old behavior, using the current
|
||||||
|
datetime as the start of unscheduled tasks.
|
||||||
|
- ~nil~: Never add a start time for unscheduled tasks. For repeating
|
||||||
|
tasks this technically violates the iCalendar spec, but some
|
||||||
|
iCalendar programs support this usage.
|
||||||
|
|
||||||
** New features
|
** New features
|
||||||
*** ~org-insert-todo-heading-respect-content~ now accepts prefix arguments
|
*** ~org-insert-todo-heading-respect-content~ now accepts prefix arguments
|
||||||
|
|
||||||
|
@ -230,6 +267,33 @@ editing with Emacs while a ~:session~ block executes.
|
||||||
When ~org-return-follows-link~ is non-nil and cursor is over an
|
When ~org-return-follows-link~ is non-nil and cursor is over an
|
||||||
org-cite citation, ~org-return~ will call ~org-open-at-point~.
|
org-cite citation, ~org-return~ will call ~org-open-at-point~.
|
||||||
|
|
||||||
|
*** Add support for repeating tasks in iCalendar export
|
||||||
|
|
||||||
|
Repeating Scheduled and Deadline timestamps in TODOs are now exported
|
||||||
|
as recurring tasks in iCalendar export.
|
||||||
|
|
||||||
|
In case the TODO has just a single planning timestamp (Scheduled or
|
||||||
|
Deadline, but not both), its repeater is used as the iCalendar
|
||||||
|
recurrence rule (RRULE).
|
||||||
|
|
||||||
|
If the TODO has both Scheduled and Deadline planning timestamps, then
|
||||||
|
the following cases are implemented:
|
||||||
|
|
||||||
|
- If both have the same repeater, then it is used as the RRULE.
|
||||||
|
- Scheduled has repeater but Deadline does not: the Scheduled repeater
|
||||||
|
is used as RRULE, and Deadline is used as UNTIL (the end date for
|
||||||
|
the repeater). This is similar to ~repeated-after-deadline~ in
|
||||||
|
~org-agenda-skip-scheduled-if-deadline-is-shown~.
|
||||||
|
|
||||||
|
The following 2 cases are not yet implemented, and the repeater is
|
||||||
|
skipped (with a warning) if the ox-icalendar export encounters them:
|
||||||
|
|
||||||
|
- Deadline has a repeater but Scheduled does not.
|
||||||
|
- Scheduled and Deadline have different repeaters.
|
||||||
|
|
||||||
|
Also note that only vanilla repeaters are currently exported; the
|
||||||
|
special repeaters ~++~ and ~.+~ are skipped.
|
||||||
|
|
||||||
** Miscellaneous
|
** Miscellaneous
|
||||||
*** =org-crypt.el= now applies initial visibility settings to decrypted entries
|
*** =org-crypt.el= now applies initial visibility settings to decrypted entries
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
;; - non-footnote definitions in footnote section,
|
;; - non-footnote definitions in footnote section,
|
||||||
;; - probable invalid keywords,
|
;; - probable invalid keywords,
|
||||||
;; - invalid blocks,
|
;; - invalid blocks,
|
||||||
|
;; - mismatched repeaters in planning info line,
|
||||||
;; - misplaced planning info line,
|
;; - misplaced planning info line,
|
||||||
;; - probable incomplete drawers,
|
;; - probable incomplete drawers,
|
||||||
;; - probable indented diary-sexps,
|
;; - probable indented diary-sexps,
|
||||||
|
@ -905,6 +906,34 @@ Use \"export %s\" instead"
|
||||||
"Name \"%s\" contains a colon; Babel cannot use it as input"
|
"Name \"%s\" contains a colon; Babel cannot use it as input"
|
||||||
name)))))))
|
name)))))))
|
||||||
|
|
||||||
|
(defun org-lint-mismatched-planning-repeaters (ast)
|
||||||
|
(org-element-map ast 'planning
|
||||||
|
(lambda (e)
|
||||||
|
(let* ((scheduled (org-element-property :scheduled e))
|
||||||
|
(deadline (org-element-property :deadline e))
|
||||||
|
(scheduled-repeater-type (org-element-property
|
||||||
|
:repeater-type scheduled))
|
||||||
|
(deadline-repeater-type (org-element-property
|
||||||
|
:repeater-type deadline))
|
||||||
|
(scheduled-repeater-value (org-element-property
|
||||||
|
:repeater-value scheduled))
|
||||||
|
(deadline-repeater-value (org-element-property
|
||||||
|
:repeater-value deadline)))
|
||||||
|
(when (and scheduled deadline
|
||||||
|
(memq scheduled-repeater-type '(cumulate catch-up))
|
||||||
|
(memq deadline-repeater-type '(cumulate catch-up))
|
||||||
|
(> scheduled-repeater-value 0)
|
||||||
|
(> deadline-repeater-value 0)
|
||||||
|
(not
|
||||||
|
(and
|
||||||
|
(eq scheduled-repeater-type deadline-repeater-type)
|
||||||
|
(eq (org-element-property :repeater-unit scheduled)
|
||||||
|
(org-element-property :repeater-unit deadline))
|
||||||
|
(eql scheduled-repeater-value deadline-repeater-value))))
|
||||||
|
(list
|
||||||
|
(org-element-property :begin e)
|
||||||
|
"Different repeaters in SCHEDULED and DEADLINE timestamps."))))))
|
||||||
|
|
||||||
(defun org-lint-misplaced-planning-info (_)
|
(defun org-lint-misplaced-planning-info (_)
|
||||||
(let ((case-fold-search t)
|
(let ((case-fold-search t)
|
||||||
reports)
|
reports)
|
||||||
|
@ -1516,6 +1545,11 @@ AST is the buffer parse tree."
|
||||||
#'org-lint-invalid-block
|
#'org-lint-invalid-block
|
||||||
:trust 'low)
|
:trust 'low)
|
||||||
|
|
||||||
|
(org-lint-add-checker 'mismatched-planning-repeaters
|
||||||
|
"Report mismatched repeaters in planning info line"
|
||||||
|
#'org-lint-mismatched-planning-repeaters
|
||||||
|
:trust 'low)
|
||||||
|
|
||||||
(org-lint-add-checker 'misplaced-planning-info
|
(org-lint-add-checker 'misplaced-planning-info
|
||||||
"Report misplaced planning info line"
|
"Report misplaced planning info line"
|
||||||
#'org-lint-misplaced-planning-info
|
#'org-lint-misplaced-planning-info
|
||||||
|
|
|
@ -168,8 +168,9 @@ This is a list with possibly several symbols in it. Valid symbols are:
|
||||||
|
|
||||||
`todo-start'
|
`todo-start'
|
||||||
|
|
||||||
Scheduling time stamps in TODO entries become start date. Some
|
Scheduling time stamps in TODO entries become start date. (See
|
||||||
calendar applications show TODO entries only after that date."
|
also `org-icalendar-todo-unscheduled-start', which controls the
|
||||||
|
start date for TODO entries without a scheduling time stamp)"
|
||||||
:group 'org-export-icalendar
|
:group 'org-export-icalendar
|
||||||
:type
|
:type
|
||||||
'(set :greedy t
|
'(set :greedy t
|
||||||
|
@ -231,6 +232,38 @@ t include tasks that are not in DONE state.
|
||||||
(repeat :tag "Specific TODO keywords"
|
(repeat :tag "Specific TODO keywords"
|
||||||
(string :tag "Keyword"))))
|
(string :tag "Keyword"))))
|
||||||
|
|
||||||
|
(defcustom org-icalendar-todo-unscheduled-start 'recurring-deadline-warning
|
||||||
|
"Exported start date of unscheduled TODOs.
|
||||||
|
|
||||||
|
If `org-icalendar-use-scheduled' contains `todo-start' and a task
|
||||||
|
has a \"SCHEDULED\" timestamp, that is always used as the start
|
||||||
|
date. Otherwise, this variable controls whether a start date is
|
||||||
|
exported and what its value is.
|
||||||
|
|
||||||
|
Note that the iCalendar spec RFC 5545 does not generally require
|
||||||
|
tasks to have a start date, except for repeating tasks which do
|
||||||
|
require a start date. However some iCalendar programs ignore the
|
||||||
|
requirement for repeating tasks, and allow repeating deadlines
|
||||||
|
without a matching start date.
|
||||||
|
|
||||||
|
This variable has no effect when `org-icalendar-include-todo' is nil.
|
||||||
|
|
||||||
|
Valid values are:
|
||||||
|
`recurring-deadline-warning' If deadline repeater present,
|
||||||
|
use `org-deadline-warning-days' as start.
|
||||||
|
`deadline-warning' If deadline present,
|
||||||
|
use `org-deadline-warning-days' as start.
|
||||||
|
`current-datetime' Use the current date-time as start.
|
||||||
|
nil Never add a start time for unscheduled tasks."
|
||||||
|
:group 'org-export-icalendar
|
||||||
|
:type '(choice
|
||||||
|
(const :tag "Warning days if deadline recurring" recurring-deadline-warning)
|
||||||
|
(const :tag "Warning days if deadline present" deadline-warning)
|
||||||
|
(const :tag "Now" current-datetime)
|
||||||
|
(const :tag "No start date" nil))
|
||||||
|
:package-version '(Org . "9.7")
|
||||||
|
:safe #'symbolp)
|
||||||
|
|
||||||
(defcustom org-icalendar-include-bbdb-anniversaries nil
|
(defcustom org-icalendar-include-bbdb-anniversaries nil
|
||||||
"Non-nil means a combined iCalendar file should include anniversaries.
|
"Non-nil means a combined iCalendar file should include anniversaries.
|
||||||
The anniversaries are defined in the BBDB database."
|
The anniversaries are defined in the BBDB database."
|
||||||
|
@ -731,6 +764,13 @@ inlinetask within the section."
|
||||||
;; Don't forget components from inner entries.
|
;; Don't forget components from inner entries.
|
||||||
contents))))
|
contents))))
|
||||||
|
|
||||||
|
(defun org-icalendar--rrule (unit value)
|
||||||
|
(format "RRULE:FREQ=%s;INTERVAL=%d"
|
||||||
|
(cl-case unit
|
||||||
|
(hour "HOURLY") (day "DAILY") (week "WEEKLY")
|
||||||
|
(month "MONTHLY") (year "YEARLY"))
|
||||||
|
value))
|
||||||
|
|
||||||
(defun org-icalendar--vevent
|
(defun org-icalendar--vevent
|
||||||
(entry timestamp uid summary location description categories timezone class)
|
(entry timestamp uid summary location description categories timezone class)
|
||||||
"Create a VEVENT component.
|
"Create a VEVENT component.
|
||||||
|
@ -756,12 +796,11 @@ Return VEVENT component as a string."
|
||||||
(org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
|
(org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
|
||||||
(org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
|
(org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
|
||||||
;; RRULE.
|
;; RRULE.
|
||||||
(when (org-element-property :repeater-type timestamp)
|
(when (org-element-property :repeater-type timestamp)
|
||||||
(format "RRULE:FREQ=%s;INTERVAL=%d\n"
|
(concat (org-icalendar--rrule
|
||||||
(cl-case (org-element-property :repeater-unit timestamp)
|
(org-element-property :repeater-unit timestamp)
|
||||||
(hour "HOURLY") (day "DAILY") (week "WEEKLY")
|
(org-element-property :repeater-value timestamp))
|
||||||
(month "MONTHLY") (year "YEARLY"))
|
"\n"))
|
||||||
(org-element-property :repeater-value timestamp)))
|
|
||||||
"SUMMARY:" summary "\n"
|
"SUMMARY:" summary "\n"
|
||||||
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
|
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
|
||||||
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
|
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
|
||||||
|
@ -772,6 +811,23 @@ Return VEVENT component as a string."
|
||||||
(org-icalendar--valarm entry timestamp summary)
|
(org-icalendar--valarm entry timestamp summary)
|
||||||
"END:VEVENT")))
|
"END:VEVENT")))
|
||||||
|
|
||||||
|
(defun org-icalendar--repeater-type (elem)
|
||||||
|
"Return ELEM's repeater-type if supported, else warn and return nil."
|
||||||
|
(let ((repeater-value (org-element-property :repeater-value elem))
|
||||||
|
(repeater-type (org-element-property :repeater-type elem)))
|
||||||
|
(cond
|
||||||
|
((not (and repeater-type
|
||||||
|
repeater-value
|
||||||
|
(> repeater-value 0)))
|
||||||
|
nil)
|
||||||
|
;; TODO Add catch-up to supported repeaters (use EXDATE to implement)
|
||||||
|
((not (memq repeater-type '(cumulate)))
|
||||||
|
(org-display-warning
|
||||||
|
(format "Repeater-type %s not currently supported by iCalendar export"
|
||||||
|
(symbol-name repeater-type)))
|
||||||
|
nil)
|
||||||
|
(repeater-type))))
|
||||||
|
|
||||||
(defun org-icalendar--vtodo
|
(defun org-icalendar--vtodo
|
||||||
(entry uid summary location description categories timezone class)
|
(entry uid summary location description categories timezone class)
|
||||||
"Create a VTODO component.
|
"Create a VTODO component.
|
||||||
|
@ -784,27 +840,101 @@ task. CATEGORIES defines the categories the task belongs to.
|
||||||
TIMEZONE specifies a time zone for this TODO only.
|
TIMEZONE specifies a time zone for this TODO only.
|
||||||
|
|
||||||
Return VTODO component as a string."
|
Return VTODO component as a string."
|
||||||
(let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled)
|
(let* ((sc (and (memq 'todo-start org-icalendar-use-scheduled)
|
||||||
(org-element-property :scheduled entry))
|
(org-element-property :scheduled entry)))
|
||||||
;; If we can't use a scheduled time for some
|
(dl (and (memq 'todo-due org-icalendar-use-deadline)
|
||||||
;; reason, start task now.
|
(org-element-property :deadline entry)))
|
||||||
(let ((now (decode-time)))
|
(sc-repeat-p (org-icalendar--repeater-type sc))
|
||||||
(list 'timestamp
|
(dl-repeat-p (org-icalendar--repeater-type dl))
|
||||||
(list :type 'active
|
(repeat-value (or (org-element-property :repeater-value sc)
|
||||||
:minute-start (nth 1 now)
|
(org-element-property :repeater-value dl)))
|
||||||
:hour-start (nth 2 now)
|
(repeat-unit (or (org-element-property :repeater-unit sc)
|
||||||
:day-start (nth 3 now)
|
(org-element-property :repeater-unit dl)))
|
||||||
:month-start (nth 4 now)
|
(repeat-until (and sc-repeat-p (not dl-repeat-p) dl))
|
||||||
:year-start (nth 5 now)))))))
|
(start
|
||||||
|
(cond
|
||||||
|
(sc)
|
||||||
|
((eq org-icalendar-todo-unscheduled-start 'current-datetime)
|
||||||
|
(let ((now (decode-time)))
|
||||||
|
(list 'timestamp
|
||||||
|
(list :type 'active
|
||||||
|
:minute-start (nth 1 now)
|
||||||
|
:hour-start (nth 2 now)
|
||||||
|
:day-start (nth 3 now)
|
||||||
|
:month-start (nth 4 now)
|
||||||
|
:year-start (nth 5 now)))))
|
||||||
|
((or (and (eq org-icalendar-todo-unscheduled-start
|
||||||
|
'deadline-warning)
|
||||||
|
dl)
|
||||||
|
(and (eq org-icalendar-todo-unscheduled-start
|
||||||
|
'recurring-deadline-warning)
|
||||||
|
dl-repeat-p))
|
||||||
|
(let ((dl-raw (org-element-property :raw-value dl)))
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert dl-raw)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(org-timestamp-down-day (org-get-wdays dl-raw))
|
||||||
|
(org-element-timestamp-parser)))))))
|
||||||
(concat "BEGIN:VTODO\n"
|
(concat "BEGIN:VTODO\n"
|
||||||
"UID:TODO-" uid "\n"
|
"UID:TODO-" uid "\n"
|
||||||
(org-icalendar-dtstamp) "\n"
|
(org-icalendar-dtstamp) "\n"
|
||||||
(org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
|
(when start (concat (org-icalendar-convert-timestamp
|
||||||
(and (memq 'todo-due org-icalendar-use-deadline)
|
start "DTSTART" nil timezone)
|
||||||
(org-element-property :deadline entry)
|
"\n"))
|
||||||
(concat (org-icalendar-convert-timestamp
|
(when (and dl (not repeat-until))
|
||||||
(org-element-property :deadline entry) "DUE" nil timezone)
|
(concat (org-icalendar-convert-timestamp
|
||||||
"\n"))
|
dl "DUE" nil timezone)
|
||||||
|
"\n"))
|
||||||
|
;; RRULE
|
||||||
|
(cond
|
||||||
|
;; SCHEDULED, DEADLINE have different repeaters
|
||||||
|
((and dl-repeat-p
|
||||||
|
(not (and (eq repeat-value (org-element-property
|
||||||
|
:repeater-value dl))
|
||||||
|
(eq repeat-unit (org-element-property
|
||||||
|
:repeater-unit dl)))))
|
||||||
|
;; TODO Implement via RDATE with changing DURATION
|
||||||
|
(org-display-warning "Not yet implemented: \
|
||||||
|
different repeaters on SCHEDULED and DEADLINE. Skipping.")
|
||||||
|
nil)
|
||||||
|
;; DEADLINE has repeater but SCHEDULED doesn't
|
||||||
|
((and dl-repeat-p (and sc (not sc-repeat-p)))
|
||||||
|
;; TODO SCHEDULED should only apply to first instance;
|
||||||
|
;; use RDATE with custom DURATION to implement that
|
||||||
|
(org-display-warning "Not yet implemented: \
|
||||||
|
repeater on DEADLINE but not SCHEDULED. Skipping.")
|
||||||
|
nil)
|
||||||
|
((or sc-repeat-p dl-repeat-p)
|
||||||
|
(concat
|
||||||
|
(org-icalendar--rrule repeat-unit repeat-value)
|
||||||
|
;; add UNTIL part to RRULE
|
||||||
|
(when repeat-until
|
||||||
|
(let* ((start-time
|
||||||
|
(org-element-property :minute-start start))
|
||||||
|
;; RFC5545 requires UTC iff DTSTART is not local time
|
||||||
|
(local-time-p
|
||||||
|
(and (not timezone)
|
||||||
|
(equal org-icalendar-date-time-format
|
||||||
|
":%Y%m%dT%H%M%S")))
|
||||||
|
(encoded
|
||||||
|
(org-encode-time
|
||||||
|
0
|
||||||
|
(or (org-element-property :minute-start repeat-until)
|
||||||
|
0)
|
||||||
|
(or (org-element-property :hour-start repeat-until)
|
||||||
|
0)
|
||||||
|
(org-element-property :day-start repeat-until)
|
||||||
|
(org-element-property :month-start repeat-until)
|
||||||
|
(org-element-property :year-start repeat-until))))
|
||||||
|
(concat ";UNTIL="
|
||||||
|
(cond
|
||||||
|
((not start-time)
|
||||||
|
(format-time-string "%Y%m%d" encoded))
|
||||||
|
(local-time-p
|
||||||
|
(format-time-string "%Y%m%dT%H%M%S" encoded))
|
||||||
|
((format-time-string "%Y%m%dT%H%M%SZ"
|
||||||
|
encoded t))))))
|
||||||
|
"\n")))
|
||||||
"SUMMARY:" summary "\n"
|
"SUMMARY:" summary "\n"
|
||||||
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
|
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
|
||||||
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
|
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
|
||||||
|
|
|
@ -406,6 +406,13 @@ This is not a node property
|
||||||
(org-test-with-temp-text "#+name: name\n| a |"
|
(org-test-with-temp-text "#+name: name\n| a |"
|
||||||
(org-lint '(colon-in-name)))))
|
(org-lint '(colon-in-name)))))
|
||||||
|
|
||||||
|
(ert-deftest test-org-lint/mismatched-planning-repeaters ()
|
||||||
|
"Test `org-lint-mismatched-planning-repeaters' checker."
|
||||||
|
(should
|
||||||
|
(org-test-with-temp-text "* H
|
||||||
|
DEADLINE: <2023-03-26 Sun +2w> SCHEDULED: <2023-03-26 Sun +1w>"
|
||||||
|
(org-lint '(mismatched-planning-repeaters)))))
|
||||||
|
|
||||||
(ert-deftest test-org-lint/misplaced-planning-info ()
|
(ert-deftest test-org-lint/misplaced-planning-info ()
|
||||||
"Test `org-lint-misplaced-planning-info' checker."
|
"Test `org-lint-misplaced-planning-info' checker."
|
||||||
(should
|
(should
|
||||||
|
|
|
@ -40,5 +40,93 @@
|
||||||
(should (eql 1 (coding-system-eol-type last-coding-system-used))))
|
(should (eql 1 (coding-system-eol-type last-coding-system-used))))
|
||||||
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
||||||
|
|
||||||
|
(ert-deftest test-ox-icalendar/todo-repeater-shared ()
|
||||||
|
"Test shared repeater on todo scheduled and deadline."
|
||||||
|
(let* ((org-icalendar-include-todo 'all)
|
||||||
|
(tmp-ics (org-test-with-temp-text-in-file
|
||||||
|
"* TODO Both repeating
|
||||||
|
DEADLINE: <2023-04-02 Sun +1m> SCHEDULED: <2023-03-26 Sun +1m>"
|
||||||
|
(expand-file-name (org-icalendar-export-to-ics)))))
|
||||||
|
(unwind-protect
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents tmp-ics)
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "DTSTART;VALUE=DATE:20230326")))
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "DUE;VALUE=DATE:20230402")))
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "RRULE:FREQ=MONTHLY;INTERVAL=1"))))
|
||||||
|
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
||||||
|
|
||||||
|
(ert-deftest test-ox-icalendar/todo-repeating-deadline-warndays ()
|
||||||
|
"Test repeating deadline with DTSTART as warning days."
|
||||||
|
(let* ((org-icalendar-include-todo 'all)
|
||||||
|
(org-icalendar-todo-unscheduled-start 'recurring-deadline-warning)
|
||||||
|
(tmp-ics (org-test-with-temp-text-in-file
|
||||||
|
"* TODO Repeating deadline
|
||||||
|
DEADLINE: <2023-04-02 Sun +2w -3d>"
|
||||||
|
(expand-file-name (org-icalendar-export-to-ics)))))
|
||||||
|
(unwind-protect
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents tmp-ics)
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "DTSTART;VALUE=DATE:20230330")))
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "DUE;VALUE=DATE:20230402")))
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "RRULE:FREQ=WEEKLY;INTERVAL=2"))))
|
||||||
|
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
||||||
|
|
||||||
|
(ert-deftest test-ox-icalendar/todo-repeater-until ()
|
||||||
|
"Test repeater on todo scheduled until deadline."
|
||||||
|
(let* ((org-icalendar-include-todo 'all)
|
||||||
|
(tmp-ics (org-test-with-temp-text-in-file
|
||||||
|
"* TODO Repeating scheduled with nonrepeating deadline
|
||||||
|
DEADLINE: <2023-05-01 Mon> SCHEDULED: <2023-03-26 Sun +3d>"
|
||||||
|
(expand-file-name (org-icalendar-export-to-ics)))))
|
||||||
|
(unwind-protect
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents tmp-ics)
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "DTSTART;VALUE=DATE:20230326")))
|
||||||
|
(save-excursion
|
||||||
|
(should (not (re-search-forward "^DUE" nil t))))
|
||||||
|
(save-excursion
|
||||||
|
(should (search-forward "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=20230501"))))
|
||||||
|
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
||||||
|
|
||||||
|
(ert-deftest test-ox-icalendar/todo-repeater-until-utc ()
|
||||||
|
"Test that UNTIL is in UTC when DTSTART is not in local time format."
|
||||||
|
(let* ((org-icalendar-include-todo 'all)
|
||||||
|
(org-icalendar-date-time-format ":%Y%m%dT%H%M%SZ")
|
||||||
|
(tmp-ics (org-test-with-temp-text-in-file
|
||||||
|
"* TODO Repeating scheduled with nonrepeating deadline
|
||||||
|
DEADLINE: <2023-05-02 Tue> SCHEDULED: <2023-03-26 Sun 15:00 +3d>"
|
||||||
|
(expand-file-name (org-icalendar-export-to-ics)))))
|
||||||
|
(unwind-protect
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents tmp-ics)
|
||||||
|
(save-excursion
|
||||||
|
(should (re-search-forward "DTSTART:2023032.T..0000")))
|
||||||
|
(save-excursion
|
||||||
|
(should (not (re-search-forward "^DUE" nil t))))
|
||||||
|
(save-excursion
|
||||||
|
(should (re-search-forward "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=2023050.T..0000Z"))))
|
||||||
|
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
|
||||||
|
|
||||||
|
(ert-deftest test-ox-icalendar/warn-unsupported-repeater ()
|
||||||
|
"Test warning is emitted for unsupported repeater type."
|
||||||
|
(let ((org-icalendar-include-todo 'all))
|
||||||
|
(should
|
||||||
|
(member
|
||||||
|
"Repeater-type restart not currently supported by iCalendar export"
|
||||||
|
(org-test-capture-warnings
|
||||||
|
(let ((tmp-ics (org-test-with-temp-text-in-file
|
||||||
|
"* TODO Unsupported restart repeater
|
||||||
|
SCHEDULED: <2023-03-26 Sun .+1m>"
|
||||||
|
(expand-file-name (org-icalendar-export-to-ics)))))
|
||||||
|
(when (file-exists-p tmp-ics)
|
||||||
|
(delete-file tmp-ics))))))))
|
||||||
|
|
||||||
(provide 'test-ox-icalendar)
|
(provide 'test-ox-icalendar)
|
||||||
;;; test-ox-icalendar.el ends here
|
;;; test-ox-icalendar.el ends here
|
||||||
|
|
Loading…
Reference in New Issue