summaryrefslogtreecommitdiff
path: root/rt-liberation.el
diff options
context:
space:
mode:
Diffstat (limited to 'rt-liberation.el')
-rw-r--r--rt-liberation.el907
1 files changed, 655 insertions, 252 deletions
diff --git a/rt-liberation.el b/rt-liberation.el
index f30e048..4ee51c9 100644
--- a/rt-liberation.el
+++ b/rt-liberation.el
@@ -1,11 +1,11 @@
-;;; rt-liberation.el --- Emacs interface to RT
+;;; rt-liberation.el --- Emacs interface to RT -*- lexical-binding: t; -*-
;; Copyright (C) 2008-2020 Free Software Foundation, Inc.
;; Author: Yoni Rabkin <yrk@gnu.org>
;; Authors: Aaron S. Hawley <aaron.s.hawley@gmail.com>, John Sullivan <johnsu01@wjsullivan.net>
;; Maintainer: Yoni Rabkin <yrk@gnu.org>
-;; Version: 1.31
+;; Version: 2.01
;; Keywords: rt, tickets
;; Package-Type: multi
;; url: http://www.nongnu.org/rtliber/
@@ -38,13 +38,16 @@
;;; Code:
-
(require 'browse-url)
(require 'time-date)
(require 'cl-lib)
(require 'rt-liberation-rest)
+(declare-function rt-liber-get-ancillary-text "rt-liberation-storage.el")
+(declare-function rt-liber-ticket-marked-p "rt-liberation-multi.el")
+(declare-function rt-liber-set-ancillary-text "rt-liberation-storage.el")
+
(defgroup rt-liber nil
"*rt-liberation, the Emacs interface to RT"
@@ -56,6 +59,24 @@
:type 'string
:group 'rt-liber)
+(defvar rt-liber-viewer-section-header-regexp
+ "^# [0-9]+/[0-9]+ (id/[0-9]+/total)")
+
+(defvar rt-liber-viewer-section-field-regexp
+ "^\\(.+\\): \\(.+\\)$")
+
+(defconst rt-liber-viewer-font-lock-keywords
+ (let ((header-regexp (regexp-opt '("id: " "Ticket: " "TimeTaken: "
+ "Type: " "Field: " "OldValue: "
+ "NewValue: " "Data: "
+ "Description: " "Created: "
+ "Creator: " "Attachments: ")
+ t)))
+ (list
+ (list (concat "^" header-regexp ".*$") 0
+ 'font-lock-comment-face)))
+ "Expressions to font-lock for RT ticket viewer.")
+
(defvar rt-liber-created-string "Created"
"String representation of \"created\" query tag.")
@@ -112,7 +133,6 @@
(defvar rt-liber-browser-default-filter-function
'rt-liber-default-filter-f
"Default filtering function.
-
This is a function which accepts the ticket alist as a single
argument and returns nil if the ticket needs to be filtered out,
dropped or ignored (however you wish to put it.), otherwise the
@@ -163,17 +183,6 @@ function returns a truth value.")
(t (:background "Black")))
"Face for high priority tickets in browser buffer.")
-(defconst rt-liber-viewer-font-lock-keywords
- (let ((header-regexp (regexp-opt '("id: " "Ticket: " "TimeTaken: "
- "Type: " "Field: " "OldValue: "
- "NewValue: " "Data: "
- "Description: " "Created: "
- "Creator: " "Attachments: ") t)))
- (list
- (list (concat "^" header-regexp ".*$") 0
- 'font-lock-comment-face)))
- "Expressions to font-lock for RT ticket viewer.")
-
(defvar rt-liber-browser-do-refresh t
"When t, run `rt-liber-browser-refresh' otherwise disable it.")
@@ -193,7 +202,6 @@ server.")
(status . "Status")
(priority . "Priority"))
"Mapping between field symbols and RT field strings.
-
The field symbols provide the programmer with a consistent way of
referring to RT fields.")
@@ -203,14 +211,12 @@ referring to RT fields.")
(open . "open")
(new . "new"))
"Mapping between status symbols and status strings.
-
The status symbols provide the programmer with a consistent way
of referring to certain statuses. The status strings are the
server specific strings.")
(defvar rt-liber-debug-log-enable nil
"If t then enable logging of communication to a buffer.
-
Careful! This might create a sizable buffer.")
(defvar rt-liber-debug-log-buffer-name "*rt-liber debug log*"
@@ -218,19 +224,35 @@ Careful! This might create a sizable buffer.")
(defvar rt-liber-ticket-local nil
"Buffer local storage for a ticket.
-
This variable is made buffer local for the ticket history")
(defvar rt-liber-assoc-browser nil
"Browser associated with a ticket history.
-
This variable is made buffer local for the ticket history")
+(defcustom rt-liber-gnus-comment-address "no comment address set"
+ "*Email address for adding a comment."
+ :type 'string
+ :group 'rt-liber-gnus)
+
+(defcustom rt-liber-gnus-address "no reply address set"
+ "*Email address for replying to requestor."
+ :type 'string
+ :group 'rt-liber-gnus)
+
+(defvar rt-liber-display-ticket-at-point-f 'rt-liber-viewer2-display-ticket-at-point
+ "Function for displaying ticket at point in the browser.")
+
+(defvar rt-liber-viewer2-section-regexp "^Ticket [0-9]+ by "
+ "Regular expression to match a section in the viewer.")
+
+(defvar rt-liber-viewer2-recenter 4
+ "Argument passed to `recenter' in the viewer.")
+
;;; --------------------------------------------------------
;;; Debug log
;;; --------------------------------------------------------
-
(defun rt-liber-debug-log-write (str)
"Write STR to debug log."
(when (not (stringp str))
@@ -244,7 +266,6 @@ This variable is made buffer local for the ticket history")
;;; --------------------------------------------------------
;;; TicketSQL compiler
;;; --------------------------------------------------------
-
(defun rt-liber-bool-p (sym)
"Return t if SYM is a boolean operator, otherwise nil."
(member sym '(and or)))
@@ -352,7 +373,6 @@ AFTER date after predicate."
;;; --------------------------------------------------------
;;; Parse Answer
;;; --------------------------------------------------------
-
(defun rt-liber-parse-answer (answer-string parser-f)
"Operate on ANSWER-STRING with PARSER-F."
(with-temp-buffer
@@ -367,7 +387,6 @@ AFTER date after predicate."
;;; --------------------------------------------------------
;;; Ticket list retriever
;;; --------------------------------------------------------
-
(put 'rt-liber-no-result-from-query-error
'error-conditions
'(error rt-liber-errors rt-liber-no-result-from-query-error))
@@ -435,7 +454,6 @@ AFTER date after predicate."
;;; --------------------------------------------------------
;;; Ticket utilities
;;; --------------------------------------------------------
-
(defun rt-liber-ticket-days-old (ticket-alist)
"Return the age of the ticket in positive days."
(days-between (format-time-string "%Y-%m-%dT%T%z" (current-time))
@@ -445,26 +463,6 @@ AFTER date after predicate."
(<= rt-liber-ticket-old-threshold
(rt-liber-ticket-days-old ticket-alist)))
-
-;;; --------------------------------------------------------
-;;; Ticket viewer
-;;; --------------------------------------------------------
-
-(defun rt-liber-jump-to-latest-correspondence ()
- "Move point to the newest correspondence section."
- (interactive)
- (let (latest-point)
- (save-excursion
- (goto-char (point-max))
- (when (re-search-backward rt-liber-correspondence-regexp
- (point-min) t)
- (setq latest-point (point))))
- (if latest-point
- (progn
- (goto-char latest-point)
- (rt-liber-next-section-in-viewer))
- (message "no correspondence found"))))
-
(defun rt-liber-ticket-id-only (ticket-alist)
"Return numerical portion of ticket number from TICKET-ALIST."
(if ticket-alist
@@ -480,11 +478,6 @@ AFTER date after predicate."
nil))
nil))
-(defun rt-liber-get-field-string (field-symbol)
- (when (not field-symbol)
- (error "null field symbol"))
- (cdr (assoc field-symbol rt-liber-field-dictionary)))
-
(defun rt-liber-ticket-owner-only (ticket-alist)
"Return the string value of the ticket owner."
(when (not ticket-alist)
@@ -492,202 +485,15 @@ AFTER date after predicate."
(cdr (assoc (rt-liber-get-field-string 'owner)
ticket-alist)))
-(defun rt-liber-viewer-visit-in-browser ()
- "Visit this ticket in the RT Web interface."
- (interactive)
- (let ((id (rt-liber-ticket-id-only rt-liber-ticket-local)))
- (if id
- (browse-url
- (concat rt-liber-base-url "Ticket/Display.html?id=" id))
- (error "no ticket currently in view"))))
-
-(defun rt-liber-viewer-mode-quit ()
- "Bury the ticket viewer."
- (interactive)
- (bury-buffer))
-
-(defun rt-liber-viewer-show-ticket-browser ()
- "Return to the ticket browser buffer."
- (interactive)
- (let ((id (rt-liber-ticket-id-only rt-liber-ticket-local)))
- (if id
- (let ((target-buffer
- (if rt-liber-assoc-browser
- (buffer-name rt-liber-assoc-browser)
- (buffer-name rt-liber-browser-buffer-name))))
- (if target-buffer
- (switch-to-buffer target-buffer)
- (error "associated ticket browser buffer no longer exists"))
- (rt-liber-browser-move-point-to-ticket id))
- (error "no ticket currently in view"))))
-
-(defun rt-liber-next-section-in-viewer ()
- "Move point to next section."
- (interactive)
- (forward-line 1)
- (when (not (re-search-forward rt-liber-content-regexp (point-max) t))
- (message "no next section"))
- (goto-char (point-at-bol)))
-
-(defun rt-liber-previous-section-in-viewer ()
- "Move point to previous section."
- (interactive)
- (forward-line -1)
- (when (not (re-search-backward rt-liber-content-regexp (point-min) t))
- (message "no previous section"))
- (goto-char (point-at-bol)))
-
-(defconst rt-liber-viewer-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "q") 'rt-liber-viewer-mode-quit)
- (define-key map (kbd "n") 'rt-liber-next-section-in-viewer)
- (define-key map (kbd "N") 'rt-liber-jump-to-latest-correspondence)
- (define-key map (kbd "p") 'rt-liber-previous-section-in-viewer)
- (define-key map (kbd "V") 'rt-liber-viewer-visit-in-browser)
- (define-key map (kbd "m") 'rt-liber-viewer-answer)
- (define-key map (kbd "M") 'rt-liber-viewer-answer-this)
- (define-key map (kbd "t") 'rt-liber-viewer-answer-provisionally)
- (define-key map (kbd "T") 'rt-liber-viewer-answer-provisionally-this)
- (define-key map (kbd "F") 'rt-liber-viewer-answer-verbatim-this)
- (define-key map (kbd "c") 'rt-liber-viewer-comment)
- (define-key map (kbd "C") 'rt-liber-viewer-comment-this)
- (define-key map (kbd "g") 'revert-buffer)
- (define-key map (kbd "SPC") 'scroll-up)
- (define-key map (kbd "DEL") 'scroll-down)
- (define-key map (kbd "h") 'rt-liber-viewer-show-ticket-browser)
- map)
- "Key map for ticket viewer.")
-
-(define-derived-mode rt-liber-viewer-mode nil
- "RT Liberation Viewer"
- "Major Mode for viewing RT tickets.
-\\{rt-liber-viewer-mode-map}"
- (set
- (make-local-variable 'font-lock-defaults)
- '((rt-liber-viewer-font-lock-keywords)))
- (set (make-local-variable 'revert-buffer-function)
- #'rt-liber-refresh-ticket-history)
- (set (make-local-variable 'buffer-stale-function)
- (lambda (&optional _noconfirm) 'slow))
- (when rt-liber-jump-to-latest
- (rt-liber-jump-to-latest-correspondence))
- (run-hooks 'rt-liber-viewer-hook))
-
-(defun rt-liber-display-ticket-history (ticket-alist &optional assoc-browser)
- "Display history for ticket.
-
-TICKET-ALIST alist of ticket data.
-ASSOC-BROWSER if non-nil should be a ticket browser."
- (let* ((ticket-id (rt-liber-ticket-id-only ticket-alist))
- (contents (rt-liber-rest-run-ticket-history-base-query ticket-id))
- (new-ticket-buffer (get-buffer-create
- (concat "*RT Ticket #" ticket-id "*"))))
- (with-current-buffer new-ticket-buffer
- (let ((inhibit-read-only t))
- (erase-buffer)
- (insert contents)
- (goto-char (point-min))
- (rt-liber-viewer-mode)
- (set
- (make-local-variable 'rt-liber-ticket-local)
- ticket-alist)
- (when assoc-browser
- (set
- (make-local-variable 'rt-liber-assoc-browser)
- assoc-browser))
- (set-buffer-modified-p nil)
- (setq buffer-read-only t)))
- (switch-to-buffer new-ticket-buffer)))
-
-(defun rt-liber-refresh-ticket-history (&optional _ignore-auto _noconfirm)
- (interactive)
- (if rt-liber-ticket-local
- (rt-liber-display-ticket-history rt-liber-ticket-local
- rt-liber-assoc-browser)
- (error "not viewing a ticket")))
-
-;; wrapper functions around specific functions provided by a backend
-
-(declare-function
- rt-liber-gnus-compose-reply-to-requestor
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-reply-to-requestor-to-this
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-reply-to-requestor-verbatim-this
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-provisional
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-provisional-to-this
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-comment
- "rt-liberation-gnus.el")
-(declare-function
- rt-liber-gnus-compose-comment-this
- "rt-liberation-gnus.el")
-
-(defun rt-liber-viewer-answer ()
- "Answer the ticket."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-reply-to-requestor))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-answer-this ()
- "Answer the ticket using the current context."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-reply-to-requestor-to-this))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-answer-verbatim-this ()
- "Answer the ticket using the current context verbatim."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-reply-to-requestor-verbatim-this))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-answer-provisionally ()
- "Provisionally answer the ticket."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-provisional))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-answer-provisionally-this ()
- "Provisionally answer the ticket using the current context."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-provisional-to-this))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-comment ()
- "Comment on the ticket."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-comment))
- (t (error "no function defined"))))
-
-(defun rt-liber-viewer-comment-this ()
- "Comment on the ticket using the current context."
- (interactive)
- (cond ((featurep 'rt-liberation-gnus)
- (rt-liber-gnus-compose-comment-this))
- (t (error "no function defined"))))
+(defun rt-liber-get-field-string (field-symbol)
+ (when (not field-symbol)
+ (error "null field symbol"))
+ (cdr (assoc field-symbol rt-liber-field-dictionary)))
;;; --------------------------------------------------------
;;; Ticket browser
;;; --------------------------------------------------------
-
-(declare-function
- rt-liber-get-ancillary-text
- "rt-liberation-storage.el")
-
;; accept a ticket-alist object and return an alist mapping ticket
;; properties to format characters for use in `rt-liber-format'.
(defun rt-liber-format-function (ticket-alist)
@@ -768,9 +574,6 @@ The ticket's priority is compared to the variable
'(face font-lock-comment-face)))
(newline))
-(declare-function rt-liber-ticket-marked-p
- "rt-liberation-multi.el")
-
(defun rt-liber-ticketlist-browser-redraw (ticketlist &optional query)
"Display TICKETLIST. Optionally display QUERY as well."
(erase-buffer)
@@ -861,6 +664,11 @@ If POINT is nil then called on (point)."
(let ((ticket-alist (get-text-property (point) 'rt-ticket)))
(rt-liber-display-ticket-history ticket-alist (current-buffer))))
+(defun rt-liber-ticket-at-point ()
+ "Display the contents of the ticket at point."
+ (interactive)
+ (funcall rt-liber-display-ticket-at-point-f))
+
(defun rt-liber-browser-search (id)
"Return point where ticket with ID is displayed or nil."
(let ((p nil))
@@ -895,7 +703,6 @@ If POINT is nil then called on (point)."
;;; --------------------------------------------------------
;;; Ticket browser sorting
;;; --------------------------------------------------------
-
(defun rt-liber-lex-lessthan-p (a b field)
"Return t if A is lexicographically less than B in FIELD."
(let ((field-a (cdr (assoc field a)))
@@ -938,7 +745,6 @@ If POINT is nil then called on (point)."
;;; --------------------------------------------------------
;;; Ticket browser filtering
;;; --------------------------------------------------------
-
;; See the fine manual for example code.
(defun rt-liber-default-filter-f (_ticket)
@@ -952,7 +758,6 @@ and as such always return t."
;;; --------------------------------------------------------
;;; Entry points
;;; --------------------------------------------------------
-
(defun rt-liber-browse-query (query &optional new)
"Run QUERY against the server and launch the browser.
@@ -995,7 +800,6 @@ returned as no associated text properties."
;;; --------------------------------------------------------
;;; Major mode definitions
;;; --------------------------------------------------------
-
(defun rt-liber-browser-mode-quit ()
"Bury the ticket browser."
(interactive)
@@ -1006,7 +810,7 @@ returned as no associated text properties."
(define-key map (kbd "q") 'rt-liber-browser-mode-quit)
(define-key map (kbd "n") 'rt-liber-next-ticket-in-browser)
(define-key map (kbd "p") 'rt-liber-previous-ticket-in-browser)
- (define-key map (kbd "RET") 'rt-liber-display-ticket-at-point)
+ (define-key map (kbd "RET") 'rt-liber-ticket-at-point)
(define-key map (kbd "g") 'revert-buffer)
(define-key map (kbd "G") 'rt-liber-browser-refresh-and-return)
(define-key map (kbd "a") 'rt-liber-browser-assign)
@@ -1081,8 +885,6 @@ returned as no associated text properties."
(switch-to-buffer rt-liber-browser-buffer)
(setq buffer-read-only t))
-(declare-function rt-liber-set-ancillary-text "rt-liberation-storage.el")
-
(defun rt-liber-browser-ancillary-text ()
"Wrapper function around storage backend."
(interactive)
@@ -1096,7 +898,6 @@ returned as no associated text properties."
;;; --------------------------------------------------------
;;; Command module
;;; --------------------------------------------------------
-
(defun rt-liber-command-get-dictionary-value (sym dic)
"Utility function for retrieving alist values."
(let ((value (cdr (assoc sym dic))))
@@ -1210,6 +1011,608 @@ returned as no associated text properties."
(rt-liber-browser-assign rt-liber-username))
+;;; --------------------------------------------------------
+;;; Viewer
+;;; --------------------------------------------------------
+(defun rt-liber-display-ticket-history (ticket-alist &optional assoc-browser)
+ "Display history for ticket.
+TICKET-ALIST alist of ticket data.
+ASSOC-BROWSER if non-nil should be a ticket browser."
+ (let* ((ticket-id (rt-liber-ticket-id-only ticket-alist))
+ (contents (rt-liber-rest-run-ticket-history-base-query ticket-id))
+ (new-ticket-buffer (get-buffer-create
+ (concat "*RT Ticket #" ticket-id "*"))))
+ (with-current-buffer new-ticket-buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert contents)
+ (goto-char (point-min))
+ (rt-liber-viewer-mode)
+ (set
+ (make-local-variable 'rt-liber-ticket-local)
+ ticket-alist)
+ (when assoc-browser
+ (set
+ (make-local-variable 'rt-liber-assoc-browser)
+ assoc-browser))
+ (set-buffer-modified-p nil)
+ (setq buffer-read-only t)))
+ (switch-to-buffer new-ticket-buffer)))
+
+
+;;; ------------------------------------------------------------------
+;;; viewer mode functions
+;;; ------------------------------------------------------------------
+(defun rt-liber-refresh-ticket-history (&optional _ignore-auto _noconfirm)
+ (interactive)
+ (if rt-liber-ticket-local
+ (rt-liber-display-ticket-history rt-liber-ticket-local
+ rt-liber-assoc-browser)
+ (error "not viewing a ticket")))
+
+(defun rt-liber-jump-to-latest-correspondence ()
+ "Move point to the newest correspondence section."
+ (interactive)
+ (let (latest-point)
+ (save-excursion
+ (goto-char (point-max))
+ (when (re-search-backward rt-liber-correspondence-regexp
+ (point-min) t)
+ (setq latest-point (point))))
+ (if latest-point
+ (progn
+ (goto-char latest-point)
+ (rt-liber-next-section-in-viewer))
+ (message "no correspondence found"))))
+
+(defun rt-liber-viewer-visit-in-browser ()
+ "Visit this ticket in the RT Web interface."
+ (interactive)
+ (let ((id (rt-liber-ticket-id-only rt-liber-ticket-local)))
+ (if id
+ (browse-url
+ (concat rt-liber-base-url "Ticket/Display.html?id=" id))
+ (error "no ticket currently in view"))))
+
+(defun rt-liber-viewer-mode-quit ()
+ "Bury the ticket viewer."
+ (interactive)
+ (bury-buffer))
+
+(defun rt-liber-viewer-show-ticket-browser ()
+ "Return to the ticket browser buffer."
+ (interactive)
+ (let ((id (rt-liber-ticket-id-only rt-liber-ticket-local)))
+ (if id
+ (let ((target-buffer
+ (if rt-liber-assoc-browser
+ (buffer-name rt-liber-assoc-browser)
+ (buffer-name rt-liber-browser-buffer-name))))
+ (if target-buffer
+ (switch-to-buffer target-buffer)
+ (error "associated ticket browser buffer no longer exists"))
+ (rt-liber-browser-move-point-to-ticket id))
+ (error "no ticket currently in view"))))
+
+(defun rt-liber-next-section-in-viewer ()
+ "Move point to next section."
+ (interactive)
+ (forward-line 1)
+ (when (not (re-search-forward rt-liber-content-regexp (point-max) t))
+ (message "no next section"))
+ (goto-char (point-at-bol)))
+
+(defun rt-liber-previous-section-in-viewer ()
+ "Move point to previous section."
+ (interactive)
+ (forward-line -1)
+ (when (not (re-search-backward rt-liber-content-regexp (point-min) t))
+ (message "no previous section"))
+ (goto-char (point-at-bol)))
+
+(defconst rt-liber-viewer-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "q") 'rt-liber-viewer-mode-quit)
+ (define-key map (kbd "n") 'rt-liber-next-section-in-viewer)
+ (define-key map (kbd "N") 'rt-liber-jump-to-latest-correspondence)
+ (define-key map (kbd "p") 'rt-liber-previous-section-in-viewer)
+ (define-key map (kbd "V") 'rt-liber-viewer-visit-in-browser)
+ (define-key map (kbd "m") 'rt-liber-viewer-answer)
+ (define-key map (kbd "M") 'rt-liber-viewer-answer-this)
+ (define-key map (kbd "t") 'rt-liber-viewer-answer-provisionally)
+ (define-key map (kbd "T") 'rt-liber-viewer-answer-provisionally-this)
+ (define-key map (kbd "F") 'rt-liber-viewer-answer-verbatim-this)
+ (define-key map (kbd "c") 'rt-liber-viewer-comment)
+ (define-key map (kbd "C") 'rt-liber-viewer-comment-this)
+ (define-key map (kbd "g") 'revert-buffer)
+ (define-key map (kbd "SPC") 'scroll-up)
+ (define-key map (kbd "DEL") 'scroll-down)
+ (define-key map (kbd "h") 'rt-liber-viewer-show-ticket-browser)
+ map)
+ "Key map for ticket viewer.")
+
+(define-derived-mode rt-liber-viewer-mode nil
+ "RT Liberation Viewer"
+ "Major Mode for viewing RT tickets.
+\\{rt-liber-viewer-mode-map}"
+ (set
+ (make-local-variable 'font-lock-defaults)
+ '((rt-liber-viewer-font-lock-keywords)))
+ (set (make-local-variable 'revert-buffer-function)
+ #'rt-liber-refresh-ticket-history)
+ (set (make-local-variable 'buffer-stale-function)
+ (lambda (&optional _noconfirm) 'slow))
+ (when rt-liber-jump-to-latest
+ (rt-liber-jump-to-latest-correspondence))
+ (run-hooks 'rt-liber-viewer-hook))
+
+;; wrapper functions around specific functions provided by a backend
+(declare-function
+ rt-liber-gnus-compose
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-reply-to-requestor
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-reply-to-requestor-to-this
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-reply-to-requestor-verbatim-this
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-provisional
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-provisional-to-this
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-comment
+ "rt-liberation-gnus.el")
+(declare-function
+ rt-liber-gnus-compose-comment-this
+ "rt-liberation-gnus.el")
+
+(defun rt-liber-viewer-answer ()
+ "Answer the ticket."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-reply-to-requestor))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-answer-this ()
+ "Answer the ticket using the current context."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-reply-to-requestor-to-this))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-answer-verbatim-this ()
+ "Answer the ticket using the current context verbatim."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-reply-to-requestor-verbatim-this))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-answer-provisionally ()
+ "Provisionally answer the ticket."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-provisional))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-answer-provisionally-this ()
+ "Provisionally answer the ticket using the current context."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-provisional-to-this))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-comment ()
+ "Comment on the ticket."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-comment))
+ (t (error "no function defined"))))
+
+(defun rt-liber-viewer-comment-this ()
+ "Comment on the ticket using the current context."
+ (interactive)
+ (cond ((featurep 'rt-liberation-gnus)
+ (rt-liber-gnus-compose-comment-this))
+ (t (error "no function defined"))))
+
+
+;;; ------------------------------------------------------------------
+;;; viewer2
+;;; ------------------------------------------------------------------
+
+;; Comment: The goal is to eventually break this code away to its own
+;; file.
+
+(defface rt-liber-ticket-emph-face
+ '((((class color) (background dark))
+ (:foreground "gray53"))
+ (((class color) (background light))
+ (:foreground "gray65"))
+ (((type tty) (class mono))
+ (:inverse-video t))
+ (t (:background "Blue")))
+ "Face for important text.")
+
+(defconst rt-liber-viewer2-font-lock-keywords
+ `(("^$" 0 'rt-liber-ticket-subdued-face))
+ "Expressions to font-lock for RT ticket viewer.")
+
+
+(defun rt-liber-viewer2-vernacular-plural (time)
+ "Add an ess as needed."
+ (if (= time 1)
+ ""
+ "s"))
+
+(defun rt-liber-viewer2-vernacular-date (date)
+ "Return a vernacular time delta."
+ (let* ((now (format-time-string "%Y-%m-%dT%T%z" (current-time)))
+ (days-ago (days-between now date)))
+ (cond ((= 0 days-ago)
+ "today")
+ ((< 0 days-ago 7)
+ (format "%s day%s ago" days-ago
+ (rt-liber-viewer2-vernacular-plural days-ago)))
+ ((< 7 days-ago 30)
+ (let ((weeks (floor (/ days-ago 7.0))))
+ (format "%s week%s ago"
+ weeks
+ (rt-liber-viewer2-vernacular-plural weeks))))
+ ((< 30 days-ago 365)
+ (let ((months (floor (/ days-ago 30.0))))
+ (format "%s month%s ago"
+ months
+ (rt-liber-viewer2-vernacular-plural months))))
+ (t (let ((years (floor (/ days-ago 365.0))))
+ (format "%s year%s ago"
+ years
+ (rt-liber-viewer2-vernacular-plural years)))))))
+
+(defun rt-liber-viewer2-mode-quit ()
+ "Bury the ticket viewer."
+ (interactive)
+ (bury-buffer))
+
+(defun rt-liber-viewer-reduce (section-list f acc)
+ "A Not Invented Here tail-recursive reduce function."
+ (cond ((null (cdr section-list)) acc)
+ (t (rt-liber-viewer-reduce (cdr section-list)
+ f
+ (append acc (list
+ (funcall f
+ (car section-list)
+ (cadr section-list))))))))
+
+;; According to:
+;; "https://rt-wiki.bestpractical.com/wiki/REST#Ticket_History_Entry"
+;; id: <history-id>
+;; Ticket: <ticket-id>
+;; TimeTaken: <...>
+;; Type: <...>
+;; Field: <...>
+;; OldValue: <...>
+;; NewValue: <...>
+;; Data: <...>
+;; Description: <...>
+
+;; Content: <lin1-0>
+;; <line-1>
+;; ...
+;; <line-n>
+
+;; Creator: <...>
+;; Created: <...>
+;; Attachments: <...>
+(defun rt-liber-viewer-parse-section (start end)
+ (goto-char start)
+ (when (not (re-search-forward
+ rt-liber-viewer-section-header-regexp
+ end t))
+ (error "invalid section"))
+ (forward-line 2)
+ (let (section-field-alist
+ (rt-field-list
+ '(id Ticket TimeTaken Type Field
+ OldValue NewValue Data Description
+ Creator Created)))
+ ;; definitely error out if any of this doesn't work
+ (setq section-field-alist
+ (mapcar
+ (lambda (field-symbol)
+ (re-search-forward (format "^%s:" (symbol-name field-symbol)) end nil)
+ (cons field-symbol (buffer-substring (1+ (point)) (point-at-eol))))
+ rt-field-list))
+ ;; content
+ (goto-char start)
+ (let ((content-start (re-search-forward "^Content: " end nil))
+ (content-end (progn
+ (re-search-forward "^Creator: " end nil)
+ (point-at-bol))))
+ (append section-field-alist
+ `(,(cons 'Content
+ (buffer-substring content-start
+ content-end)))))))
+
+;; According to:
+;; "https://rt-wiki.bestpractical.com/wiki/REST#Ticket_History" is of
+;; the form: "# <n>/<n> (id/<history-id>/total)"
+(defun rt-liber-viewer-parse-history (ticket-history)
+ "Parse the string TICKET-HISTORY."
+ (when (not (stringp ticket-history))
+ (error "invalid ticket-history"))
+ (with-temp-buffer
+ (insert ticket-history)
+ (goto-char (point-min))
+ ;; find history detail sections and procude a list of section
+ ;; (start . end) pairs
+ (let (section-point-list
+ section-list)
+ (while (re-search-forward rt-liber-viewer-section-header-regexp (point-max) t)
+ (setq section-point-list (append section-point-list
+ (list (point-at-bol)))))
+ (when (not section-point-list)
+ (error "no history detail sections found"))
+ (setq section-point-list (append section-point-list
+ (list (point-max)))
+ section-point-list (rt-liber-viewer-reduce section-point-list #'cons nil))
+ ;; collect the sections
+ (setq section-list
+ (mapcar
+ (lambda (section-points)
+ (rt-liber-viewer-parse-section
+ (car section-points)
+ (cdr section-points)))
+ section-point-list))
+ section-list)))
+
+(defun rt-liber-viewer2-get-section-data ()
+ "Return the current section data."
+ (let ((section (get-text-property (point) 'rt-liberation-section-data)))
+ (when (not section)
+ (save-excursion
+ (rt-liber-viewer2-previous-section-in)
+ (setq section (get-text-property (point) 'rt-liberation-section-data))))
+ section))
+
+(defun rt-liber-viewer2-format-content (content)
+ "Wrangle RT's content format."
+ (with-temp-buffer
+ (insert content)
+ (goto-char (point-min))
+ (if (re-search-forward "^This transaction appears to have no content" (point-max) t)
+ "" ; make no content mean... no content
+ ;; trim leading blank lines
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward "[[:graph:]]" (point-max) t)
+ (forward-line -1)
+ (flush-lines "^[[:space:]]+$" (point-min) (point)))
+ ;; trim trailing blank lines
+ (save-excursion
+ (goto-char (point-max))
+ (re-search-backward "[[:graph:]]" (point-min) t)
+ (forward-line 2)
+ (flush-lines "^[[:space:]]+$" (point-max) (point)))
+ ;; Convert the 9 leading whitespaces from RT's comment lines.
+ (goto-char (point-min))
+ (insert " ")
+ (while (re-search-forward "^ " (point-max) t)
+ (replace-match " "))
+ ;; fill
+ (let ((paragraph-separate " >[[:space:]]+$"))
+ (fill-region (point-min)
+ (point-max)))
+ ;; finally
+ (buffer-substring (point-min)
+ (point-max)))))
+
+(defun rt-liber-viewer2-clean-content (section)
+ "Format section content for email."
+ (with-temp-buffer
+ (insert (rt-liber-viewer2-format-content
+ (alist-get 'Content section)))
+ (goto-char (point-min))
+ (while (re-search-forward "^ " (point-max) t)
+ (replace-match ""))
+ ;; fill
+ (let ((paragraph-separate ">[[:space:]]+$"))
+ (fill-region (point-min)
+ (point-max)))
+ ;; finally
+ (buffer-substring (point-min)
+ (point-max))))
+
+(defun rt-liber-viewer2-display-section (section)
+ (let ((start (point))
+ (ticket-id (alist-get 'Ticket section))
+ (creator (alist-get 'Creator section))
+ (date (alist-get 'Created section))
+ (type (alist-get 'Type section))
+ (oldvalue (alist-get 'OldValue section))
+ (newvalue (alist-get 'NewValue section))
+ (field (alist-get 'Field section))
+ (s-content (rt-liber-viewer2-format-content
+ (alist-get 'Content section))))
+ (when (not (or (string= type "CommentEmailRecord")
+ (string= type "EmailRecord")))
+ (insert
+ (format "Ticket %s by %s, %s (%s) [%s]\n"
+ ticket-id
+ creator
+ (rt-liber-viewer2-vernacular-date date)
+ date
+ (format "%s%s%s"
+ type
+ (if (< 0 (length oldvalue))
+ (concat " " oldvalue)
+ "")
+ (if (< 0 (length newvalue))
+ (concat "->" newvalue)
+ ""))))
+ (add-text-properties start
+ (point)
+ `(font-lock-face rt-liber-ticket-emph-face))
+ (add-text-properties start
+ (point)
+ `(rt-liberation-viewer-header t))
+ (add-text-properties start
+ (point)
+ `(rt-liberation-section-data ,section))
+ (cond ((string= type "Status")
+ (insert
+ (format "\n Status change from %s to %s\n\n" oldvalue newvalue)))
+ ((and (string= type "Set")
+ (string= field "Owner")
+ (string= oldvalue "10"))
+ (insert
+ (format "\n Ticket assigned\n\n")))
+ ;; catch-all
+ (t
+ (insert
+ (format "\n%s\n" s-content)))))))
+
+(defun rt-liber-viewer2-display-history (contents)
+ (let ((section-list (rt-liber-viewer-parse-history contents)))
+ (mapc
+ (lambda (section)
+ (rt-liber-viewer2-display-section section))
+ section-list)))
+
+(defun rt-liber-viewer2-display-ticket-at-point ()
+ "Display the contents of the ticket at point."
+ (interactive)
+ (let ((ticket-alist (get-text-property (point) 'rt-ticket)))
+ (rt-liber-viewer2-display-ticket-history ticket-alist (current-buffer))))
+
+(defun rt-liber-viewer2-display-ticket-history (ticket-alist &optional assoc-browser)
+ "Display history for ticket.
+TICKET-ALIST alist of ticket data.
+ASSOC-BROWSER if non-nil should be a ticket browser."
+ (let* ((ticket-id (rt-liber-ticket-id-only ticket-alist))
+ (contents (rt-liber-rest-run-ticket-history-base-query ticket-id))
+ (new-ticket-buffer (get-buffer-create
+ (concat "*RT (Viewer) Ticket #" ticket-id "*"))))
+ (with-current-buffer new-ticket-buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (rt-liber-viewer2-display-history contents)
+ (goto-char (point-min))
+ (rt-liber-viewer2-mode)
+ (set
+ (make-local-variable 'rt-liber-ticket-local)
+ ticket-alist)
+ (when assoc-browser
+ (set
+ (make-local-variable 'rt-liber-assoc-browser)
+ assoc-browser))
+ (set-buffer-modified-p nil)
+ (setq buffer-read-only t)))
+ (switch-to-buffer new-ticket-buffer)))
+
+(defun rt-liber-viewer2-refresh-ticket-history (&optional _ignore-auto _noconfirm)
+ (interactive)
+ (if rt-liber-ticket-local
+ (rt-liber-viewer2-display-ticket-history rt-liber-ticket-local
+ rt-liber-assoc-browser)
+ (error "not viewing a ticket")))
+
+(defun rt-liber-viewer2-next-section-in ()
+ (interactive)
+ (when (looking-at rt-liber-viewer2-section-regexp)
+ (goto-char (1+ (point))))
+ (let ((next (re-search-forward rt-liber-viewer2-section-regexp
+ (point-max)
+ t)))
+ (cond ((not next)
+ (message "no next section"))
+ (t
+ (recenter rt-liber-viewer2-recenter)))
+ (goto-char (point-at-bol))))
+
+(defun rt-liber-viewer2-last-section-in ()
+ (interactive)
+ (goto-char (point-max))
+ (let ((last (re-search-backward rt-liber-viewer2-section-regexp
+ (point-min)
+ t)))
+ (if (not last)
+ (error "no sections found")
+ (recenter rt-liber-viewer2-recenter)
+ (goto-char (point-at-bol)))))
+
+(defun rt-liber-viewer2-previous-section-in ()
+ (interactive)
+ (when (looking-at rt-liber-viewer2-section-regexp)
+ (goto-char (1- (point))))
+ (let ((prev (re-search-backward rt-liber-viewer2-section-regexp
+ (point-min)
+ t)))
+ (cond ((not prev)
+ (message "no previous section"))
+ (t
+ (recenter rt-liber-viewer2-recenter)))
+ (goto-char (point-at-bol))))
+
+(defun rt-liber-viewer2-answer ()
+ (interactive)
+ (let ((section (rt-liber-viewer2-get-section-data)))
+ (when (not section)
+ (error "no section found"))
+ (if (not (featurep 'rt-liberation-gnus))
+ (error "rt-liberation-gnus feature not found")
+ (rt-liber-gnus-compose
+ rt-liber-gnus-address
+ rt-liber-ticket-local
+ `((contents . ,(rt-liber-viewer2-clean-content section)))))))
+
+(defun rt-liber-viewer2-comment ()
+ (interactive)
+ (let ((section (rt-liber-viewer2-get-section-data)))
+ (when (not section)
+ (error "no section found"))
+ (if (not (featurep 'rt-liberation-gnus))
+ (error "rt-liberation-gnus feature not found")
+ (rt-liber-gnus-compose
+ rt-liber-gnus-comment-address
+ rt-liber-ticket-local
+ `((contents . ,(rt-liber-viewer2-clean-content section)))))))
+
+(defconst rt-liber-viewer2-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "q") 'rt-liber-viewer2-mode-quit)
+ (define-key map (kbd "N") 'rt-liber-viewer2-last-section-in)
+ (define-key map (kbd "n") 'rt-liber-viewer2-next-section-in)
+ (define-key map (kbd "p") 'rt-liber-viewer2-previous-section-in)
+ (define-key map (kbd "V") 'rt-liber-viewer-visit-in-browser)
+ (define-key map (kbd "M") 'rt-liber-viewer2-answer)
+ (define-key map (kbd "C") 'rt-liber-viewer2-comment)
+ (define-key map (kbd "g") 'revert-buffer)
+ (define-key map (kbd "SPC") 'scroll-up)
+ (define-key map (kbd "DEL") 'scroll-down)
+ (define-key map (kbd "h") 'rt-liber-viewer-show-ticket-browser)
+ map)
+ "Key map for ticket viewer2.")
+
+(define-derived-mode rt-liber-viewer2-mode nil
+ "RT Liberation Viewer"
+ "Major Mode for viewing RT tickets.
+\\{rt-liber-viewer-mode-map}"
+ (set
+ (make-local-variable 'font-lock-defaults)
+ '((rt-liber-viewer2-font-lock-keywords)))
+ (set (make-local-variable 'revert-buffer-function)
+ #'rt-liber-viewer2-refresh-ticket-history)
+ (set (make-local-variable 'buffer-stale-function)
+ (lambda (&optional _noconfirm) 'slow))
+ (run-hooks 'rt-liber-viewer-hook))
+
+
(provide 'rt-liberation)
;;; rt-liberation.el ends here.