myoji-accent/entry.lisp

343 lines
13 KiB
Common Lisp

(in-package :myoji-accent/entry)
(defapp entry
:prefix "/entry"
:name "苗字アクセントを入力")
(defparameter *entries-per-page* 2
"The number of entries to be shown per page.")
(defwidget myoji-list-entry ()
((parent :initarg :parent
:accessor myoji-list-entry-parent
:documentation "The parent list of this entry.")
(id :initarg :id
:type (integer 1)
:accessor myoji-list-entry-id
:documentation "The ID of the myoji object for this entry.")
(myoji :initarg :myoji
:type myoji
:accessor myoji-list-entry-myoji
:documentation "The myoji object for this entry.")
(edit-mode :initform nil
:type boolean
:accessor myoji-list-entry-edit-mode-p
:documentation "Weather we are in edit mode or not.")
(err-msg :initform nil
:type (or null string)
:accessor myoji-list-entry-error
:documentation "An error message to show to the user.")))
(define-condition accent-error (error)
((message :initarg :message
:accessor accent-error-message
:type string
:documentation "A description of what went wrong."))
(:report
(lambda (err stream)
(format stream "~a" (accent-error-message err)))))
(defmethod update-db-entry ((widget myoji-list-entry) new-kanji new-reading
new-accent)
"Update the entry WIDGET with some new values provided by the user."
(handler-case
(progn
(setf (slot-value widget 'edit-mode) nil)
(let ((obj))
(handler-case
(let ((accent-point (parse-integer new-accent)))
(when (> accent-point (count-word-mora new-reading))
(signal
'accent-error
:message
(format nil "Accent point too bit for word: ~a" new-accent)))
(setq obj (make-myoji new-reading new-kanji
:accent-point accent-point)))
(parse-error ()
(if (valid-accent-string-p new-accent)
(setq obj (make-myoji new-reading new-kanji
:accent-string new-accent))
(signal
'accent-error
:message
(format nil "Not a valid accent string or number: ~a" new-accent)))))
(mito:insert-dao obj)
(mito:delete-dao (myoji-list-entry-myoji widget))))
(accent-error (e)
(setf (slot-value widget 'err-msg) (format nil "~a" e)))
(error ()
(setf (slot-value widget 'err-msg) "An internal error occured.")))
(reblocks/widget:update widget))
(defmethod reblocks/widget:render ((widget myoji-list-entry))
(with-slots (myoji edit-mode err-msg) widget
(with-html
(if (or edit-mode err-msg)
(with-html-form (:POST #'(lambda (&key kanji reading accent
&allow-other-keys)
(update-db-entry widget kanji reading accent)))
(:tr
(:td (:input :type "text"
:name "kanji"
:value (myoji-kanji myoji)))
(:td (:input :type "text"
:name "reading"
:value (myoji-reading myoji)))
(:td :class "accented"
(:raw (myoji-accent-html myoji)))
(:td (:input :type "text"
:name "accent"
:value (myoji-accent-point myoji)))
(:td (myoji-accent-name myoji))
(:td (:input :type "submit"
:value "編集")
(:button "削除")
(when err-msg
(:br)
(:p :class "insert-error"
err-msg)))))
(:tr
(:td (myoji-kanji myoji))
(:td (myoji-reading myoji))
(:td :class "accented"
(:raw (myoji-accent-html myoji)))
(:td (myoji-accent-point myoji))
(:td (myoji-accent-name myoji))
(:td
(:button :onclick
(when (boundp 'reblocks/page::*current-page*)
(reblocks/actions:make-js-action
(lambda (&key &allow-other-keys)
(setf (slot-value widget 'edit-mode) t)
(reblocks/widget:update (myoji-list-entry-parent widget)))))
"編集")
(:button :onclick
(when (boundp 'reblocks/page::*current-page*)
(reblocks/actions:make-js-action
(lambda (&key &allow-other-keys)
())))
"削除")))))))
(defwidget myoji-list ()
((page-num :initform 0
:type '(integer 0)
:accessor entry-page-page-num
:documentation "The index of the displayed page.")
(last-low-id :initform 0
:type '(integer 0)
:accessor entry-page-last-low-id
:documentation "The lowest ID from the last SQL query.")
(last-high-id :initform 0
:type '(integer 0)
:accessor entry-page-last-high-id
:documentation "The highest ID from the last SQL query.")
(page-entries :initform nil
:type 'list
:accessor entry-page-page-entries
:documentation "The myoji entries for the current page.")))
(defmethod initialize-instance :after
((widget myoji-list) &key &allow-other-keys)
;; get the first page
(next-page widget))
(defmethod reblocks/dependencies:get-dependencies ((widget myoji-list))
(append
(list
(reblocks-lass:make-dependency
`(.myoji-list
:margin-left auto
:margin-right auto
:width "90%"
:border "1px" "black" solid
(table
:width "100%"
:height "100%"
:font-size larger
:border-collapse collapse
:border "1px" "black" solid
((:or th td)
:border "1px" "black" solid)
((:or th td)
:padding "5px"))
(|#selector-wrapper|
:width "100%"
:display flex
:justify-content center
(span
:margin-left "5px"
:margin-right "5px")))))
(call-next-method)))
(defmethod has-next-page-p ((widget myoji-list))
"Return non-nil if WIDGET can be paged forward."
(with-slots (page-num page-entries last-high-id) widget
(or (zerop page-num)
(and (= (length page-entries) *entries-per-page*)
(mito:retrieve-by-sql
(sxql:select (:myoji.*)
(sxql:from :myoji)
(sxql:where (:> :id last-high-id))))))))
(defmethod has-previous-page-p ((widget myoji-list))
"Return non-nil if WIDGET can be paged back."
(> (slot-value widget 'page-num) 1))
(defmethod reblocks/widget:render ((widget myoji-list))
(with-slots (page-num page-entries) widget
(with-html
(:table
(:tr (dolist (header '("漢字" "読み方" "アクセント" "アクセントポイント"
"タイプ" ""))
(:th header)))
(dolist (entry page-entries)
(reblocks/widget:render entry)))
(:div :id "selector-wrapper"
(when (boundp 'reblocks/page::*current-page*)
(if (has-previous-page-p widget)
(:button :onclick (reblocks/actions:make-js-action
(lambda (&key &allow-other-keys)
(next-page widget :previous t)))
"<")
(:button :disabled "disabled"
"<"))
(:span (format nil "~a" page-num))
(if (has-next-page-p widget)
(:button :onclick (reblocks/actions:make-js-action
(lambda (&key &allow-other-keys)
(next-page widget)))
">")
(:button :disabled "disabled"
">")))))))
(defmethod update-page-from-query-results ((widget myoji-list) results)
"Update the content of the current page of WIDGET with the RESULTS, which are
SQL query results."
(with-slots (page-num last-low-id last-high-id page-entries) widget
(loop for entry in results
for id = (getf entry :id)
maximize id into new-high
minimize id into new-low
collect
(make-instance 'myoji-list-entry
:parent widget
:id id
:myoji (apply #'mito:make-dao-instance
'myoji-accent/accent:myoji
entry))
into entry-objects
finally
(setf last-low-id new-low
last-high-id new-high
page-entries entry-objects))))
(defmethod reload-page ((widget myoji-list))
"Reload the current page for WIDGET."
(with-slots (last-low-id last-high-id) widget
(update-page-from-query-results
widget
(mito:retrieve-by-sql
(sxql:select (:myoji.*)
(sxql:from :myoji)
(sxql:where (:>= :id last-low-id))
(sxql:order-by (:asc :id))
(sxql:limit *entries-per-page*))))
(reblocks/widget:update widget)))
(defmethod next-page ((widget myoji-list) &key previous)
"Display the next page for WIDGET."
(if (or (and (has-next-page-p widget) (not previous))
(and (has-previous-page-p widget) previous))
(with-slots (page-num last-low-id last-high-id) widget
(update-page-from-query-results
widget
(mito:retrieve-by-sql
(sxql:select (:myoji.*)
(sxql:from :myoji)
(if previous
(sxql:where (:< :id last-low-id))
(sxql:where (:> :id last-high-id)))
(if previous
(sxql:order-by (:desc :id))
(sxql:order-by (:asc :id)))
(sxql:limit *entries-per-page*))))
(setf page-num (if previous (1- page-num) (1+ page-num)))
(reblocks/widget:update widget))))
(defwidget entry-page ()
((list :initform (make-instance 'myoji-list)
:type 'myoji-list
:documentation "The list of already existing entries.")))
(defmethod reblocks/dependencies:get-dependencies ((widget entry-page))
(append
(list
(reblocks-lass:make-dependency
`(.entry-page
:width "100%"
(|#add-form|
:margin-top "20px"
:display "flex"
:justify-content center
((:or (:and span (:not :first-of-type)) |#submit-button|)
:margin-left "10px")
(span
:margin-right "5px"))
(.accented
(span.tone-h
:border-top "1px" solid ,*pitch-color*)
(span.tone-l
:border-bottom "1px" solid ,*pitch-color*)
(span.tone-h-change
:border solid ,*pitch-color*
:border-width "1px" "1px" "0px" "0px")
(span.tone-l-change
:border solid ,*pitch-color*
:border-width "0px" "1px" "1px" "0px")
(span.tone-end-change
:border-right "1px")))))
(call-next-method)))
(defun string-integer-p (str)
"Return non-nil if STR is all digits."
(every #'digit-char-p str))
(defmethod handle-add-myoji ((widget entry-page) reading kanji accent)
"Add a new myoji to the database."
(let ((mora (count-word-mora reading)))
(cond
((string-integer-p accent)
(mito:insert-dao (make-myoji reading kanji
:accent-point (parse-integer accent))))
((valid-accent-string-p accent mora)
(mito:insert-dao (make-myoji reading kanji
:accent-string accent)))
(t ;; TODO error
))
(reblocks/widget:update widget)))
(defmethod reblocks/widget:render ((widget entry-page))
(with-html
(reblocks/widget:render (slot-value widget 'list))
(with-html-form (:POST #'(lambda (&key reading kanji accent
&allow-other-keys)
(handle-add-myoji widget reading kanji accent))
:id "add-form")
(:span "読み方:")
(:input :type "text"
:name "reading")
(:span "漢字:")
(:input :type "text"
:name "kanji")
(:span "アクセント:")
(:input :type "text"
:name "accent")
(:input :id "submit-button"
:type "submit"
:value "追加"))))
(defmethod reblocks/page:init-page ((app entry) (url-path string) expire-at)
(declare (ignorable url-path expire-at))
(setf (reblocks/page:get-title) "苗字アクセントを入力")
(make-instance 'entry-page))