こんにちは、chibaです!
このブログでは過去に日報を如何に書くかについて度々エントリーが書かれていますが、今回も日報生成の話です。
以前は当日こなしたタスクを眺めたりして振り返る感じでしたが、タスク管理と日報の作成で仕事を二重にしている感があったので、思い切って社内のイシュー管理システムのRedmineの情報から日報を生成することにしてみました。
準備
タスクデータ取得
日報を書くには、現在抱えている仕事と、当日こなした仕事の2つの情報が必要ですが、RedmineのAPIだけでは、上手く取得できないようです。
ということでカスタムクエリを設定して、これを呼ぶことにしました。
送信
Yammerや社内メールに送信しますが、Yammerにはメール投稿用のアドレスがあるので、どちらもメールで投稿することにします。
コード
最近念願のLispWorksを購入したので無駄にGUIのインターフェイスを付けてみました。
折角なのでRedmineの状態に合せて更新するボタンも付け、弊社標準ウィンドウマネージャーのStumpWMのコマンドも作成します。
以上を合わせてこんな感じになります。
(cl:in-package :stumpwm)
(ql:quickload :cl-json :silent T)
(ql:quickload :drakma :silent T)
(ql:quickload :cl+ssl :silent T)
(ql:quickload :cl-smtp :silent T)
(defvar *firefox-path* "/usr/bin/firefox")
(defun browser (uri)
(run-shell-command
(format nil "~A ~S" *firefox-path* uri)))
(defun redmine-client (id)
(let ((drakma:*text-content-types* '(("application" . "json"))))
(drakma:http-request
(format nil "https://example.com/issues/~A.json" id)
:additional-headers '(("X-Redmine-API-Key" . "...")))))
(defun issue-subject-from-id (id)
(json:json-bind (issue.subject) (redmine-client id)
issue.subject))
(defun my-issues ()
(let ((drakma:*text-content-types* '(("application" . "json"))))
(drakma:http-request
"https://example.com/issues.json?query_id=NN"
:additional-headers
'(("X-Redmine-API-Key" . "...")))))
(defun my-issues/closed-today ()
(let ((drakma:*text-content-types* '(("application" . "json"))))
(drakma:http-request
"https://example.com/issues.json?query_id=NN"
:additional-headers
'(("X-Redmine-API-Key" . "...")))))
(defun iso-date ()
(multiple-value-bind (s- m- h- d m y)
(get-decoded-time)
(declare (ignore s- m- h-))
(format nil "~4,'0D-~2,'0D-~2,'0D" y m d)))
(defun daily-report-string ()
(let ((date (iso-date)))
(with-output-to-string (out)
(format out "~2&お疲れ様です、千葉です。")
(format out "~2&~Aの日報をお送りします。" date)
(format out "~2&【勤務時間】11:00〜17:00")
(format out "~2&【本日の作業内容】")
(json:json-bind (issues)
(my-issues/closed-today)
(when issues
(format out "~2& 【終了】~2%")
(dolist (i issues)
(format out
"~3,' D% #~A ~A~%"
(cdr (assoc :done--ratio i))
(cdr (assoc :id i))
(cdr (assoc :subject i))))))
(json:json-bind (issues)
(my-issues)
(setq issues
(sort issues
#'>
:key (lambda (x)
(cdr (assoc :done--ratio x)))))
(when issues
(format out "~2& 【実行中】~2%")
(dolist (i issues)
(unless nil ;; (zerop (cdr (assoc :done--ratio i)))
(format out
"~3,' D% #~A ~A~%"
(cdr (assoc :done--ratio i))
(cdr (assoc :id i))
(cdr (assoc :subject i)))))))
(format out "~2&【感想】~2%"))))
(defun send-yammer (sub msg)
(cl-smtp:send-email "smtp.example.com"
"送り主のアドレス"
"Yammerのメール投稿アドレス"
sub
msg
:ssl :tls
:authentication
'(:login "foouser" "foopass")))
(defconstant tue 1)
(defun tomorrow-off-p ()
(= tue (nth 6 (multiple-value-list (get-decoded-time)))))
(defun declare-off-day (offp msg)
(block nil
(or offp (return msg))
(let ((dcl (format nil "~%※明日水曜日はお休みです。~%")))
(and (search dcl msg) (return msg))
(concatenate 'string msg dcl))))
(capi:define-interface daily-report ()
()
(:panes
(editor-pane
capi:editor-pane
:flag 'daily-report
:text (declare-off-day (tomorrow-off-p) (daily-report-string))
:buffer-name :temp
:echo-area-pane echo-area
:visible-min-width '(character 80)
:visible-min-height '(character 15))
(buttons
capi:push-button-panel
:items '("Send Report" "Update")
:callback-type :data
:selection-callback
(lambda (command)
(cond ((string= "Send Report" command)
(when (capi:confirm-yes-or-no "この内容で送信しますか?")
(send-yammer (format nil
"~1&【日報 システムチーム/千葉】 ~A"
(iso-date))
(capi:editor-pane-text editor-pane))
(capi:apply-in-pane-process editor-pane
#'capi:quit-interface
editor-pane)))
(T (when (capi:confirm-yes-or-no "タスク内容を更新しますか?")
(let* ((text (capi:editor-pane-text editor-pane))
(playback (search "【感想】" text))
(updated-text (daily-report-string)))
(setf (capi:editor-pane-text editor-pane)
(declare-off-day (tomorrow-off-p)
(if playback
(concatenate 'string
(subseq updated-text
0
(+ (+ -2 (- #.(length "【感想】")))
(length updated-text)))
(subseq text playback nil))
updated-text))
)))))))
(echo-area capi:echo-area-pane :max-height t))
(:menus (quit "Quit" ("Quit") :callback (lambda (name self)
(declare (ignore name))
(capi:quit-interface self))))
(:menu-bar quit)
(:default-initargs :title "Daily Report" :auto-menus nil))
(defcommand daily-report () ()
""
(execute-command-or-raise 'daily-report1
'(:title "Daily Report")))
(defcommand daily-report1 () ()
""
(let ((ept (make-instance 'daily-report)))
(capi:display ept)))
どうもコードに重複が多いですが、まあこれで良いだろうということで完成。
30行程度でGUIが付くなんてLispWorksは素晴しいですね。