fedora-csb-system-manager
1;;; org-review.el --- Schedule reviews for Org entries
2;;
3;; Copyright (C) 2024 Alan Schmitt
4;;
5;; Author: Alan Schmitt <alan.schmitt@polytechnique.org>
6;; URL: https://github.com/brabalan/org-review
7;; Version: 0.3
8;; Keywords: calendar
9
10;; This file is not part of GNU Emacs.
11
12;; This program is free software; you can redistribute it and/or modify
13;; it under the terms of the GNU General Public License as published by
14;; the Free Software Foundation; either version 3, or (at your option)
15;; any later version.
16;;
17;; This program is distributed in the hope that it will be useful, but
18;; WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20;; General Public License for more details.
21;;
22;; You should have received a copy of the GNU General Public License
23;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
24;;
25;;; Commentary:
26;;
27;; This allows to schedule reviews of org entries.
28;;
29;; Entries will be scheduled for review if their NEXT_REVIEW or their
30;; LAST_REVIEW property is set. The next review date is the
31;; NEXT_REVIEW date, if it is present, otherwise it is computed from
32;; the LAST_REVIEW property and the REVIEW_DELAY period, such as
33;; "+1m". If REVIEW_DELAY is absent, a default period is used. Note
34;; that the LAST_REVIEW property is not considered as inherited, but
35;; REVIEW_DELAY is, allowing to set it for whole subtrees.
36;;
37;; Checking of review dates is done through an agenda view, using the
38;; `org-review-agenda-skip' skipping function. This function is based
39;; on `org-review-toreview-p', that returns `nil' if no review is
40;; necessary (no review planned or it happened recently), otherwise it
41;; returns the date the review was first necessary (NEXT_REVIEW, or
42;; LAST_REVIEW + REVIEW_DELAY, if it is in the past).
43;;
44;; To mark an entry as reviewed, use the function
45;; `org-review-insert-last-review' to set the LAST_REVIEW date to the
46;; current date. If `org-review-sets-next-date' is set (which is the
47;; default), this function also computes the date of the next review
48;; and inserts it as NEXT_REVIEW.
49;;
50;; Example use.
51;;
52;; 1 - To display the things to review in the agenda.
53;;
54;; (setq org-agenda-custom-commands (quote ( ...
55;; ("R" "Review projects" tags-todo "-CANCELLED/"
56;; ((org-agenda-overriding-header "Reviews Scheduled")
57;; (org-agenda-skip-function 'org-review-agenda-skip)
58;; (org-agenda-cmp-user-defined 'org-review-compare)
59;; (org-agenda-sorting-strategy '(user-defined-down)))) ... )))
60;;
61;; 2 - To set a key binding to review from the agenda
62;;
63;; (add-hook 'org-agenda-mode-hook (lambda () (local-set-key (kbd "C-c
64;; C-r") 'org-review-insert-last-review)))
65
66;;; Changes
67;;
68;; 2022-04-11: systematically insert name of week day in date
69;; 2016-08-18: better detection of org-agenda buffers
70;; 2014-05-08: added the ability to specify next review dates
71
72;; TODO
73;; - be able to specify a function to run when marking an item reviewed
74
75;;; Code:
76
77(require 'org)
78(require 'org-agenda)
79
80;;; User variables:
81
82(defgroup org-review nil
83 "Org review scheduling."
84 :tag "Org Review Schedule"
85 :group 'org)
86
87(defcustom org-review-last-timestamp-format 'naked
88 "Timestamp format for last review properties."
89 :type '(radio (const naked)
90 (const inactive)
91 (const active))
92 :group 'org-review)
93
94(defcustom org-review-next-timestamp-format 'naked
95 "Timestamp format for last review properties."
96 :type '(radio (const naked)
97 (const inactive)
98 (const active))
99 :group 'org-review)
100
101(defcustom org-review-last-property-name "LAST_REVIEW"
102 "The name of the property for the date of the last review."
103 :type 'string
104 :group 'org-review)
105
106(defcustom org-review-delay-property-name "REVIEW_DELAY"
107 "The name of the property for setting the delay before the next review."
108 :type 'string
109 :group 'org-review)
110
111(defcustom org-review-next-property-name "NEXT_REVIEW"
112 "The name of the property for setting the date of the next review."
113 :type 'string
114 :group 'org-review)
115
116(defcustom org-review-delay "+1m"
117 "Time span between the date of last review and the next one.
118The default value for this variable (\"+1m\") means that entries
119will be marked for review one month after their last review.
120
121If the review delay cannot be retrieved from the entry or the
122subtree above, this delay is used."
123 :type 'string
124 :group 'org-review)
125
126(defcustom org-review-sets-next-date t
127 "Indicates whether marking a project as reviewed automatically
128sets the next NEXT_REVIEW according to the current date and
129REVIEW_DELAY."
130 :type 'boolean
131 :group 'org-review)
132
133;;; Functions:
134
135(defun org-review-last-planned (last delay)
136 "Computes the next planned review, given the LAST review
137date (in string format) and the review DELAY (in string
138format)."
139 (let ((lt (org-read-date nil t last))
140 (ct (current-time)))
141 (time-add lt (time-subtract (org-read-date nil t delay) ct))))
142
143;;;###autoload
144(defun org-review-last-review-prop (&optional pos)
145 "Return the value of the last review property of the headline
146at position POS, or the current headline if POS is not given."
147 (org-entry-get (or pos (point)) org-review-last-property-name))
148
149;;;###autoload
150(defun org-review-next-review-prop (&optional pos)
151 "Return the value of the review date property of the headline
152at position POS, or the current headline if POS is not given."
153 (org-entry-get (or pos (point)) org-review-next-property-name))
154
155(defun org-review-review-delay-prop (&optional pos)
156 "Return the value of the review delay property of the headline
157at position POS, or the current headline if POS is not given,
158considering inherited properties."
159 (org-entry-get (or pos (point)) org-review-delay-property-name t))
160
161(defun org-review-toreview-p (&optional pos)
162 "Check if the entry at point should be marked for review.
163Return nil if the entry does not need to be reviewed. Otherwise
164return the date when the entry was first scheduled to be
165reviewed.
166
167If there is a next review date, consider it. Otherwise, if there
168is a last review date, use it to compute the date of the next
169review (adding the value of the review delay property, or
170`org-review-delay' if there is no review delay property). If
171there is no next review date and no last review date, return
172nil."
173 (let* ((lp (org-review-last-review-prop pos))
174 (np (org-review-next-review-prop pos))
175 (nextreview
176 (cond
177 (np (org-read-date nil t np))
178 (lp (org-review-last-planned
179 lp
180 (or (org-review-review-delay-prop pos)
181 org-review-delay)))
182 (t nil))))
183 (and nextreview
184 (time-less-p nextreview (current-time))
185 nextreview)))
186
187(defun org-review-insert-date (propname fmt date)
188 "Insert the DATE under property PROPNAME, in the format
189specified by FMT."
190 (org-entry-put
191 (if (equal major-mode 'org-agenda-mode)
192 (or (org-get-at-bol 'org-marker)
193 (org-agenda-error))
194 (point))
195 propname
196 (cond
197 ((eq fmt 'inactive)
198 (concat "[" date "]"))
199 ((eq fmt 'active)
200 (concat "<" date ">"))
201 (t date))))
202
203;;;###autoload
204(defun org-review-insert-last-review (&optional prompt)
205 "Insert the current date as last review. If prefix argument:
206prompt the user for the date. If `org-review-sets-next-date' is
207set to t, also insert a next review date."
208 (interactive "P")
209 (let ((ts (if prompt
210 (format-time-string (car org-time-stamp-formats) (org-read-date nil t))
211 (format-time-string (car org-time-stamp-formats)))))
212 (org-review-insert-date org-review-last-property-name
213 org-review-last-timestamp-format
214 ts)
215 (when org-review-sets-next-date
216 (org-review-insert-date
217 org-review-next-property-name
218 org-review-next-timestamp-format
219 (format-time-string
220 (car org-time-stamp-formats)
221 (org-review-last-planned
222 ts
223 (or (org-review-review-delay-prop
224 (if (equal major-mode 'org-agenda-mode)
225 (or (org-get-at-bol 'org-marker)
226 (org-agenda-error))
227 (point)))
228 org-review-delay)))))))
229
230;;;###autoload
231(defun org-review-insert-next-review ()
232 "Prompt the user for the date of the next review, and insert
233it as a property of the headline."
234 (interactive)
235 (let ((ts (format-time-string (car org-time-stamp-formats) (org-read-date nil t))))
236 (org-review-insert-date org-review-next-property-name
237 org-review-next-timestamp-format
238 ts)))
239
240;;;###autoload
241(defun org-review-agenda-skip ()
242 "To be used as an argument of `org-agenda-skip-function' to
243skip entries that are not scheduled to be reviewed. This function
244does not move the point; it returns nil if the entry is to be
245kept, and the position to continue the search otherwise."
246 (and (not (org-review-toreview-p))
247 (org-with-wide-buffer (or (outline-next-heading) (point-max)))))
248
249(defun org-review-compare (a b)
250 "Compares the date of scheduled review for the two agenda
251entries, to be used with `org-agenda-cmp-user-defined'. Returns
252+1 if A has been scheduled for longer and -1 otherwise."
253 (let* ((ma (or (get-text-property 0 'org-marker a)
254 (get-text-property 0 'org-hd-marker a)))
255 (mb (or (get-text-property 0 'org-marker b)
256 (get-text-property 0 'org-hd-marker b)))
257 (ra (org-review-toreview-p ma))
258 (rb (org-review-toreview-p mb)))
259 (if (time-less-p ra rb) 1 -1)))
260
261(provide 'org-review)
262
263;;; org-review.el ends here