miniblog

Miniblog: A command-line static blog system in Common Lisp
Log | Files | Refs | README | LICENSE

commit f378df5dd6db182615a4adc4e8bc251db98763e3
parent ea0de3f88915002b1d044977f12fa9ed91d9de87
Author: Decay <decay@todayiwilllaunchmyinfantsonintoorbit.com>
Date:   Mon, 12 Oct 2020 21:00:09 -0700

Starting to decouple the blog data from the DB

Diffstat:
Mminiblog.asd | 3++-
Asrc/data.lisp | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/db.lisp | 129+++++++++++++++++++++++++++-----------------------------------------------------
Msrc/miniblog.lisp | 64++++++++++++++++++++++++++++++++--------------------------------
Msrc/packages.lisp | 20++++++++++++++++++--
Atst/data.lisp | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtst/db.lisp | 217-------------------------------------------------------------------------------
7 files changed, 400 insertions(+), 338 deletions(-)

diff --git a/miniblog.asd b/miniblog.asd @@ -26,6 +26,7 @@ into date-structured directories as a normal HTML." (:file "format") (:file "edit") (:file "db") + (:file "data") (:file "content") (:file "miniblog"))) (:module "templates" @@ -53,5 +54,5 @@ into date-structured directories as a normal HTML." :components ((:file "packages") (:file "format") (:file "edit") - (:file "db")))) + (:file "data")))) :perform (test-op (o c) (symbol-call :miniblog/tests :run-miniblog-tests))) diff --git a/src/data.lisp b/src/data.lisp @@ -0,0 +1,88 @@ +(in-package :miniblog.data) + +(defgeneric xform (entry) + (:documentation "Transform an entry of some type into an idiomatic p-list")) + +(defmethod xform ((entry blog-entries)) + "Transform a blog-entries object into an idiomatic property list" + (list :id (mito.dao.mixin:object-id entry) + :created-at (mito.dao.mixin:object-created-at entry) + :last-updated-at (mito.dao.mixin:object-updated-at entry) + :title (entry-title entry) + :content (entry-content entry) + :created-by (entry-username entry) + :last-updated-by (entry-last-updated-by entry))) + +(defmethod xform ((entry blog-pages)) + "Transform a blog-pages object into an idiomatic p-list" + (list :id (mito.dao.mixin:object-id entry) + :created-at (mito.dao.mixin:object-created-at entry) + :last-updated-at (mito.dao.mixin:object-updated-at entry) + :name (page-name entry) + :parent (page-parent entry) + :title (page-title entry) + :content (page-content entry) + :created-by (page-username entry) + :last-updated-by (page-last-updated-by entry))) + +(defun add-entry (title content &key (username "nobody")) + (miniblog.db:add-entry title content :username username :transform #'xform)) + +(defun get-entry (id) + "Get entry by id, or nil if the requested id isn't found" + (miniblog.db:get-entry id :transform #'xform)) + +(defmacro with-entry-id (entry id &rest body) + "Takes a varname to hold the entry list and a post id + and executes the forms in body with the entry bound + to the specified entry variable" + `(let ((,entry (miniblog.db:get-entry ,id))) + (if ,entry + (progn ,@body) + (format t "Post ID ~d not found~%" ,id)))) + +(defun get-entries (&key year month max-entries) + "Get entries from the database, optionally limited to a date + range or count" + (miniblog.db:get-entries :year year :month month :max-entries max-entries :transform #'xform)) + +(defun update-entry (id title content &key (username "nobody")) + "Update entry by id. Returns the updated entry or nil if the id doesn't exist." + (miniblog.db:update-entry id title content :username username :transform #'xform)) + +(defun delete-entry (id) + "Delete the specified entry from the database. No-op if the id is invalid." + (miniblog.db:delete-entry id)) + +(defun add-page (name title content &key (parent 0) (username "nobody")) + "Add a new page to the database" + (miniblog.db:add-page name title content :parent parent :username username :transform #'xform)) + +(defun get-page (id) + "Get page by id, or NIL if the requested id isn't found" + (miniblog.db:get-page id :transform #'xform)) + +(defmacro with-page-id (page id &rest body) + "Takes a varname to hold the entry list and a page id + and executes the forms in body with the entry bound + to the specified entry variable" + `(let ((,page (miniblog.db:get-page ,id))) + (if ,page + (progn ,@body) + (format t "Page ID ~d not found~%" ,id)))) + +(defun get-pages (&optional (root-id 0)) + "Get all pages in a tree, or optionally a subtree starting from a given id. Returns the subtree as the first return value and the (unconditionally) full hashtable as the second." + (miniblog.db:get-pages :root-id root-id :transform #'xform)) + +(defun update-page (id name title content &key (username "nobody")) + "Update page by id. Returns the updated page or nil if the id doesn't exist." + (miniblog.db:update-page id name title content :username username :transform #'xform)) + +(defun move-page (id parent) + "Move a page under a new parent page (or to the root if parent is 0 or NIL)" + (miniblog.db:move-page id parent :transform #'xform)) + +(defun delete-page (id &key (children :move-to-parent)) + "Delete a page. The parameter CHILDREN can be any of :MOVE-TO-PARENT (the default) in which all immediate children will be attached to the deleted page's parent, :MOVE-TO-ROOT in which all immediate children will be made top-level pages, or :DELETE in which case the entire subtree will be pruned." + (miniblog.db:delete-page id :children children)) diff --git a/src/db.lisp b/src/db.lisp @@ -39,58 +39,24 @@ (apply #'connect-toplevel params) (init-tables)) -(defgeneric xform (entry) - (:documentation "Transform an entry of some type into an idiomatic p-list")) - -(defmethod xform ((entry blog-entries)) - "Transform a blog-entries object into an idiomatic property list" - (list :id (mito.dao.mixin:object-id entry) - :created-at (mito.dao.mixin:object-created-at entry) - :last-updated-at (mito.dao.mixin:object-updated-at entry) - :title (entry-title entry) - :content (entry-content entry) - :created-by (entry-username entry) - :last-updated-by (entry-last-updated-by entry))) - -(defmethod xform ((entry blog-pages)) - "Transform a blog-pages object into an idiomatic p-list" - (list :id (mito.dao.mixin:object-id entry) - :created-at (mito.dao.mixin:object-created-at entry) - :last-updated-at (mito.dao.mixin:object-updated-at entry) - :name (page-name entry) - :parent (page-parent entry) - :title (page-title entry) - :content (page-content entry) - :created-by (page-username entry) - :last-updated-by (page-last-updated-by entry))) - -(defun add-entry (title content &key (username "nobody")) +(defun add-entry (title content &key (username "nobody") (transform #'identity)) "Add a new blog entry to the database" - (xform (create-dao 'blog-entries - :title title - :content content - :username username - :last-updated-by username))) + (funcall transform (create-dao 'blog-entries + :title title + :content content + :username username + :last-updated-by username))) (defun get-raw-entry (id) (find-dao 'blog-entries :id id)) -(defun get-entry (id) +(defun get-entry (id &key (transform #'identity)) "Get entry by id, or nil if the requested id isn't found" (let ((entry (get-raw-entry id))) (if entry - (xform entry)))) - -(defmacro with-entry-id (entry id &rest body) - "Takes a varname to hold the entry list and a post id - and executes the forms in body with the entry bound - to the specified entry variable" - `(let ((,entry (miniblog.db:get-entry ,id))) - (if ,entry - (progn ,@body) - (format t "Post ID ~d not found~%" ,id)))) - -(defun get-entries (&key year month max-entries) + (funcall transform entry)))) + +(defun get-entries (&key year month max-entries (transform #'identity)) "Get entries from the database, optionally limited to a date range or count" (labels @@ -121,23 +87,23 @@ (where (:and (:>= :created_at (start-range year month)) (:< :created_at (end-range year month))))))) - (mapcar #'xform + (mapcar transform (select-dao 'blog-entries (where-clause year month) (order-by (:desc :created_at)) (if max-entries (limit max-entries)))))) -(defun update-entry (id title content &key (username "nobody")) +(defun update-entry (id title content &key (username "nobody") (transform #'identity)) "Update entry by id. Returns the updated entry or nil if the id doesn't exist." (let ((entry (get-raw-entry id))) (if entry - (xform (progn - (setf (entry-title entry) title) - (setf (entry-content entry) content) - (setf (entry-last-updated-by entry) username) - (save-dao entry) - entry))))) + (funcall transform + (progn (setf (entry-title entry) title) + (setf (entry-content entry) content) + (setf (entry-last-updated-by entry) username) + (save-dao entry) + entry))))) (defun delete-entry (id) "Delete the specified entry from the database. No-op if the id is invalid." @@ -145,40 +111,31 @@ (if entry (delete-dao entry)))) -(defun add-page (name title content &key (parent 0) (username "nobody")) +(defun add-page (name title content &key (parent 0) (username "nobody") (transform #'identity)) "Add a new page to the database" - (xform (create-dao 'blog-pages - :name name - :parent parent - :title title - :content content - :username username - :last-updated-by username))) + (funcall transform (create-dao 'blog-pages + :name name + :parent parent + :title title + :content content + :username username + :last-updated-by username))) (defun get-raw-page (id) (find-dao 'blog-pages :id id)) -(defun get-page (id) +(defun get-page (id &key (transform #'identity)) "Get page by id, or NIL if the requested id isn't found" (let ((entry (get-raw-page id))) (if entry - (xform entry)))) - -(defmacro with-page-id (page id &rest body) - "Takes a varname to hold the entry list and a page id - and executes the forms in body with the entry bound - to the specified entry variable" - `(let ((,page (miniblog.db:get-page ,id))) - (if ,page - (progn ,@body) - (format t "Page ID ~d not found~%" ,id)))) - -(defun build-tree-table (dao-list &optional (xform #'xform)) + (funcall transform entry)))) + +(defun build-tree-table (dao-list transform) (let ((entry-table (make-hash-table))) (labels ((build-tree (raw-entry) (let ((id (mito.dao.mixin:object-id raw-entry)) (parent-id (page-parent raw-entry)) - (entry (funcall xform raw-entry))) + (entry (funcall transform raw-entry))) (setf (getf entry :children) (getf (gethash id entry-table) :children)) (setf (gethash id entry-table) entry) @@ -188,33 +145,33 @@ (mapcar #'build-tree dao-list) entry-table))) -(defun get-pages (&optional (root-id 0)) +(defun get-pages (&key (root-id 0) (transform #'identity)) "Get all pages in a tree, or optionally a subtree starting from a given id. Returns the subtree as the first return value and the (unconditionally) full hashtable as the second." - (let* ((entry-table (build-tree-table (select-dao 'blog-pages)))) + (let* ((entry-table (build-tree-table (select-dao 'blog-pages) transform))) (values (gethash root-id entry-table) entry-table))) -(defun update-page (id name title content &key (username "nobody")) +(defun update-page (id name title content &key (username "nobody") (transform #'identity)) "Update page by id. Returns the updated page or nil if the id doesn't exist." (let ((entry (get-raw-page id))) (if entry - (xform (progn - (setf (page-name entry) name) - (setf (page-title entry) title) - (setf (page-content entry) content) - (setf (page-last-updated-by entry) username) - (save-dao entry) - entry))))) + (funcall transform + (progn (setf (page-name entry) name) + (setf (page-title entry) title) + (setf (page-content entry) content) + (setf (page-last-updated-by entry) username) + (save-dao entry) + entry))))) (defun move-page-dao (entry parent) (setf (page-parent entry) parent) (save-dao entry) entry) -(defun move-page (id parent) +(defun move-page (id parent &key (transform #'identity)) "Move a page under a new parent page (or to the root if parent is 0 or NIL)" (let ((entry (get-raw-page id))) (if entry - (xform (move-page-dao entry (or parent 0)))))) + (funcall transform (move-page-dao entry (or parent 0)))))) (defun prune-subtree (entry-table root &optional skip-top) (if (not skip-top) diff --git a/src/miniblog.lisp b/src/miniblog.lisp @@ -164,7 +164,7 @@ (let ((rss-content (miniblog.content:gen-rss-feed entries :generator *rss-generator* :tz *blog-timezone*)) - (pages (miniblog.db:get-pages))) + (pages (miniblog.data:get-pages))) (regenerate-file (list :rss nil rss-content)) (regenerate-file (list :index nil (miniblog.content:gen-index entries pages @@ -177,7 +177,7 @@ :tz *blog-timezone*))))) (defun regenerate-page-and-parent-and-children (parent-path page-id pages) - (let* ((entries (miniblog.db:get-entries)) + (let* ((entries (miniblog.data:get-entries)) (subtree-root (miniblog.content:get-page-by-path parent-path pages)) (archive-date-list (miniblog.content:get-archive-date-list entries *blog-timezone*)) @@ -212,8 +212,8 @@ :archive-date-list archive-date-list))))) (defun regenerate-all () - (let ((all (miniblog.content:gen-all (miniblog.db:get-entries) - (miniblog.db:get-pages) + (let ((all (miniblog.content:gen-all (miniblog.data:get-entries) + (miniblog.data:get-pages) :generator *generator* :page-generator *page-generator* :rss-generator *rss-generator* @@ -222,7 +222,7 @@ (defun entries-in-month (year month) "Determine number of entries in a given month and year" - (let ((entries-in-month (miniblog.db:get-entries :year year :month month))) + (let ((entries-in-month (miniblog.data:get-entries :year year :month month))) (length entries-in-month))) (defun new-month-p (year month) @@ -238,7 +238,7 @@ (let* ((post (miniblog.edit:get-title-and-content text)) (title (nth 0 post)) (content (nth 1 post)) - (new-entry (miniblog.db:add-entry + (new-entry (miniblog.data:add-entry (or title "Untitled") content :username (get-username))) @@ -248,7 +248,7 @@ (month (cdr year-month))) (if (or regen (new-month-p year month)) (regenerate-all) - (regenerate-index-and-given-month (miniblog.db:get-entries) year month))) + (regenerate-index-and-given-month (miniblog.data:get-entries) year month))) (format t "Abandoning post...~%"))) (defmethod add-entry ((entry-type (eql :page)) uri regen) @@ -257,13 +257,13 @@ (let* ((parent-path (butlast uri-components)) (parent-id (if parent-path (miniblog.content:get-page-id-by-path parent-path - (miniblog.db:get-pages)) + (miniblog.data:get-pages)) 0))) (if-let ((text (miniblog.edit:edit-text))) (let* ((post (miniblog.edit:get-title-and-content text)) (title (nth 0 post)) (content (nth 1 post)) - (new-entry (miniblog.db:add-page + (new-entry (miniblog.data:add-page name (or title "Untitled") content @@ -273,7 +273,7 @@ (regenerate-all) (regenerate-page-and-parent-and-children parent-path (getf new-entry :id) - (miniblog.db:get-pages)))) + (miniblog.data:get-pages)))) (format t "Abandoning page...~%"))) t) (format t "Invalid page name ~a~%" uri))) @@ -285,7 +285,7 @@ (:documentation "Get an entry of type ENTRY-TYPE with id ID")) (defmethod get-entry ((entry-type (eql :post)) id) - (miniblog.db:with-entry-id entry id + (miniblog.data:with-entry-id entry id (format t "ID: ~d~%" (getf entry :id)) (format t "Created: ~A by ~A~%" (date-format (getf entry :created-at)) (getf entry :created-by)) @@ -295,11 +295,11 @@ (format t "Content:~%~A~%" (getf entry :content)))) (defmethod get-entry ((entry-type (eql :page)) id) - (miniblog.db:with-page-id page id + (miniblog.data:with-page-id page id (format t "ID: ~d~%" (getf page :id)) (format t "Path: page/~a~%" (str:join "/" (miniblog.content:get-path-to-page id - (cadr (multiple-value-list (miniblog.db:get-pages)))))) + (cadr (multiple-value-list (miniblog.data:get-pages)))))) (format t "Created: ~A by ~A~%" (date-format (getf page :created-at)) (getf page :created-by)) (format t "Last updated: ~A by ~A~%" @@ -318,13 +318,13 @@ (:documentation "Edit an entry of type ENTRY-TYPE with id ID")) (defmethod edit-entry ((entry-type (eql :post)) id regen) - (miniblog.db:with-entry-id entry id + (miniblog.data:with-entry-id entry id (if-let ((text (miniblog.edit:edit-text :template (make-template (getf entry :title) (getf entry :content))))) (let* ((post (miniblog.edit:get-title-and-content text)) (title (nth 0 post)) (content (nth 1 post))) - (miniblog.db:update-entry + (miniblog.data:update-entry id title content :username (get-username)) (let* ((created-at (getf entry :created-at)) @@ -333,22 +333,22 @@ (month (timestamp-month created-at :timezone *blog-timezone*))) (if (not regen) - (regenerate-index-and-given-month (miniblog.db:get-entries) year month) + (regenerate-index-and-given-month (miniblog.data:get-entries) year month) (regenerate-all)))) (format t "No change, abandoning...~%")))) (defmethod edit-entry ((entry-type (eql :page)) id regen) - (miniblog.db:with-page-id entry id + (miniblog.data:with-page-id entry id (if-let ((text (miniblog.edit:edit-text :template (make-template (getf entry :title) (getf entry :content))))) (let* ((page (miniblog.edit:get-title-and-content text)) (title (nth 0 page)) (content (nth 1 page)) - (new-entry (miniblog.db:update-page + (new-entry (miniblog.data:update-page id (getf entry :name) title content :username (get-username)))) (if (not regen) - (let* ((pages-tuple (multiple-value-list (miniblog.db:get-pages))) + (let* ((pages-tuple (multiple-value-list (miniblog.data:get-pages))) (page-table (cadr pages-tuple)) (parent-id (getf new-entry :parent)) (root (gethash 0 page-table)) @@ -367,9 +367,9 @@ (defmethod delete-entry ((entry-type (eql :post)) id children-to-root regen) (declare (ignore children-to-root)) - (miniblog.db:with-entry-id entry id + (miniblog.data:with-entry-id entry id (format t "Deleting post ID ~d...~%" id) - (miniblog.db:delete-entry id) + (miniblog.data:delete-entry id) (let* ((created-at (getf entry :created-at)) (year (timestamp-year created-at :timezone *blog-timezone*)) @@ -378,22 +378,22 @@ (flush-monthly-path year month) (if (or regen (removed-month-p year month)) (regenerate-all) - (regenerate-index-and-given-month (miniblog.db:get-entries) year month))))) + (regenerate-index-and-given-month (miniblog.data:get-entries) year month))))) (defmethod delete-entry ((entry-type (eql :page)) id children-to-root regen) - (miniblog.db:with-page-id page id + (miniblog.data:with-page-id page id (format t "Delete page ID ~d...~%" id) - (let* ((pages-tuple (multiple-value-list (miniblog.db:get-pages))) + (let* ((pages-tuple (multiple-value-list (miniblog.data:get-pages))) (page-table (cadr pages-tuple)) (path (miniblog.content:get-path-to-page id page-table))) - (miniblog.db:delete-page id :children (if children-to-root - :children-to-root - :children-to-parent)) + (miniblog.data:delete-page id :children (if children-to-root + :children-to-root + :children-to-parent)) (flush-page-path path) (if (or children-to-root (< (length path) 3)) (regenerate-all) (let* ((parent-id (getf page :parent)) - (pages-tuple (multiple-value-list (miniblog.db:get-pages))) + (pages-tuple (multiple-value-list (miniblog.data:get-pages))) (page-table (cadr pages-tuple)) (parent (gethash parent-id page-table)) (parent-of-parent-id (getf parent :parent)) @@ -415,9 +415,9 @@ (parent-path (butlast uri-components)) (parent-id (miniblog.content:get-page-id-by-path parent-path - (miniblog.db:get-pages)))) + (miniblog.data:get-pages)))) - (miniblog.db:move-page id parent-id) + (miniblog.data:move-page id parent-id) ;; FIXME: Regeneration logic for moving pages is complex so ;; we're punting for now but we should avoid doing a full ;; regen unless we have to. The basic logic should look @@ -436,7 +436,7 @@ (:documentation "List entries of type ENTRY-TYPE, possibly restricted to entries START through N. For hierarchical entry types, only START is consumed, specifying which entry to start listing from in the tree.")) (defmethod list-entries ((entry-type (eql :post)) start n) - (let* ((entries (miniblog.db:get-entries)) + (let* ((entries (miniblog.data:get-entries)) (first (or start 0)) (last (if n (+ first n) @@ -453,7 +453,7 @@ (format t "~v{~a~:*~}~a (ID ~d) - \"~a\" ~a~%" depth '(" ") name id title created-by)) (loop for entry in children do (traverse entry (1+ depth)))))) - (let ((pages (miniblog.db:get-pages start))) + (let ((pages (miniblog.data:get-pages start))) (traverse pages 0)))) (defun init-tz () diff --git a/src/packages.lisp b/src/packages.lisp @@ -13,8 +13,24 @@ (defpackage :miniblog.db (:use :cl :mito :sxql) - (:export #:init #:init-tables #:add-entry #:get-entry - #:with-entry-id + (:export blog-entries blog-pages + #:entry-username #:entry-last-updated-by #:entry-title #:entry-content + #:page-username #:page-last-updated-by #:page-name #:page-parent + #:page-title #:page-content + #:init #:init-tables #:add-entry #:get-entry + #:get-entries #:update-entry #:delete-entry + #:add-page #:get-page + #:get-pages #:update-page #:delete-page + #:move-page)) + +(defpackage :miniblog.data + (:use :cl) + (:import-from :miniblog.db :blog-entries :blog-pages + :entry-username :entry-last-updated-by :entry-title + :entry-content + :page-username :page-last-updated-by :page-name :page-parent + :page-title :page-content) + (:export #:add-entry #:get-entry #:with-entry-id #:get-entries #:update-entry #:delete-entry #:add-page #:get-page #:with-page-id #:get-pages #:update-page #:delete-page diff --git a/tst/data.lisp b/tst/data.lisp @@ -0,0 +1,217 @@ +(in-package :miniblog/tests) + +(in-suite miniblog-test) + +(defparameter +db-settings+ '(sqlite3 :database-name ":memory:")) + +(test add-and-get-entry + "Add an entry and then fetch it back, verify all fields are correct" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((title "Test entry") + (content "Test content") + (username "test") + (new-entry (miniblog.data:add-entry title content :username username)) + (now (local-time:now)) + (new-id (getf new-entry :id)) + (fetched-entry (miniblog.data:get-entry new-id))) + (is (and (string= title (getf new-entry :title)) + (string= title (getf fetched-entry :title)))) + (is (and (string= content (getf new-entry :content)) + (string= content (getf fetched-entry :content)))) + (is (and (string= username (getf new-entry :created-by)) + (string= username (getf fetched-entry :created-by)))) + (is (and (string= username (getf new-entry :last-updated-by)) + (string= username (getf fetched-entry :last-updated-by)))) + (is (and (< (timestamp-difference now (getf new-entry :created-at)) 1) + (>= (timestamp-difference now (getf new-entry :created-at)) 0))) + (is (and (timestamp= (getf new-entry :created-at) + (getf new-entry :last-updated-at)))) + (is (and (timestamp= (getf new-entry :created-at) + (getf fetched-entry :created-at)))) + (is (and (timestamp= (getf new-entry :last-updated-at) + (getf fetched-entry :last-updated-at))))))) + +(test modify-entry + "Update an entry, verify update fields" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((title "Before title") + (content "Before content") + (username "before") + (entry (miniblog.data:add-entry title content :username username)) + (id (getf entry :id)) + (new-title "After title") + (new-content "After content") + (new-username "after") + (updated-entry (miniblog.data:update-entry id new-title new-content :username new-username))) + (is (string= new-title (getf updated-entry :title))) + (is (string= new-content (getf updated-entry :content))) + (is (string= username (getf updated-entry :created-by))) + (is (string= new-username (getf updated-entry :last-updated-by))) + (is (> (timestamp-difference (getf updated-entry :last-updated-at) (getf updated-entry :created-at)) 0))))) + +(test delete-entry + "Add an entry, delete it and verify it's removed" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((entry (miniblog.data:add-entry "foo" "foo")) + (id (getf entry :id))) + (is (miniblog.data:get-entry id)) + (miniblog.data:delete-entry id) + (is (not (miniblog.data:get-entry id)))))) + +(test add-and-get-page + "Add a page and then fetch it back, verify all fields are correct" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((name "test") + (title "Test page") + (content "Test content") + (username "test") + (new-page (miniblog.data:add-page name title content :username username)) + (now (local-time:now)) + (new-id (getf new-page :id)) + (fetched-page (miniblog.data:get-page new-id))) + (is (and (eql 0 (getf new-page :parent)) + (eql 0 (getf fetched-page :parent)))) + (is (and (string= name (getf new-page :name)) + (string= name (getf fetched-page :name)))) + (is (and (string= title (getf new-page :title)) + (string= title (getf fetched-page :title)))) + (is (and (string= content (getf new-page :content)) + (string= content (getf fetched-page :content)))) + (is (and (string= username (getf new-page :created-by)) + (string= username (getf fetched-page :created-by)))) + (is (and (string= username (getf new-page :last-updated-by)) + (string= username (getf fetched-page :last-updated-by)))) + (is (and (< (timestamp-difference now (getf new-page :created-at)) 1) + (>= (timestamp-difference now (getf new-page :created-at)) 0))) + (is (and (timestamp= (getf new-page :created-at) + (getf new-page :last-updated-at)))) + (is (and (timestamp= (getf new-page :created-at) + (getf fetched-page :created-at)))) + (is (and (timestamp= (getf new-page :last-updated-at) + (getf fetched-page :last-updated-at))))))) + +(test modify-page + "Update a page, verify update fields" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((name "before") + (title "Before title") + (content "Before content") + (username "before") + (page (miniblog.data:add-page name title content :username username)) + (id (getf page :id)) + (new-name "after") + (new-title "After title") + (new-content "After content") + (new-username "after") + (updated-page (miniblog.data:update-page id new-name new-title new-content :username new-username))) + (is (eql 0 (getf updated-page :parent))) + (is (string= new-title (getf updated-page :title))) + (is (string= new-content (getf updated-page :content))) + (is (string= username (getf updated-page :created-by))) + (is (string= new-username (getf updated-page :last-updated-by))) + (is (> (timestamp-difference (getf updated-page :last-updated-at) (getf updated-page :created-at)) 0))))) + +(test move-page + "Create several test pages, move one in the hierarchy and verify the move" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page-1 (miniblog.data:add-page "foo" "foo" "foo")) + (page-1-id (getf page-1 :id)) + (page-2 (miniblog.data:add-page "bar" "bar" "bar" :parent page-1-id)) + (page-2-id (getf page-2 :id)) + (page-3 (miniblog.data:add-page "baz" "baz" "baz" :parent page-2-id)) + (page-3-id (getf page-3 :id)) + (moved-page-2 (miniblog.data:move-page page-2-id 0)) + (page-3-post-move (miniblog.data:get-page page-3-id))) + (is (eql page-1-id (getf page-2 :parent))) + (is (eql 0 (getf moved-page-2 :parent))) + (is (and (eql page-2-id (getf page-3 :parent)) + (eql page-2-id (getf page-3-post-move :parent))))))) + +(test delete-page + "Add a page, delete it and verify it's removed" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page (miniblog.data:add-page "foo" "foo" "foo")) + (id (getf page :id))) + (is (miniblog.data:get-page id)) + (miniblog.data:delete-page id) + (is (not (miniblog.data:get-page id)))))) + +(test delete-page-moving-subtree-to-root + "Add a page with a child, delete it and verify that it's removed and its child is reparented to the root" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page-1 (miniblog.data:add-page "foo" "foo" "foo")) + (page-1-id (getf page-1 :id)) + (page-2 (miniblog.data:add-page "bar" "bar" "bar" :parent (getf page-1 :id))) + (page-2-id (getf page-2 :id))) + (miniblog.data:delete-page page-1-id :children :move-to-root) + (is (not (miniblog.data:get-page page-1-id))) + (is (eql 0 (getf (miniblog.data:get-page page-2-id) :parent)))))) + +(test delete-page-moving-subtree-to-parent + "Add three test pages, each the next one's parent, delete the second one and verify that it's removed and its child is reparented to first page" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page-1 (miniblog.data:add-page "foo" "foo" "foo")) + (page-1-id (getf page-1 :id)) + (page-2 (miniblog.data:add-page "bar" "bar" "bar" :parent page-1-id)) + (page-2-id (getf page-2 :id)) + (page-3 (miniblog.data:add-page "baz" "baz" "baz" :parent page-2-id)) + (page-3-id (getf page-3 :id))) + (miniblog.data:delete-page page-2-id) ; :MOVE-TO-PARENT is default + (is (eql page-1-id (getf (miniblog.data:get-page page-3-id) :parent)))))) + +(test delete-page-deleting-subtree + "Add six test pages with the first the second's parent and the second the top of a subtree containing the other four, delete the second one and verify that it's removed and all its children are as well" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page-1 (miniblog.data:add-page "foo" "foo" "foo")) + (page-1-id (getf page-1 :id)) + (page-2 (miniblog.data:add-page "bar" "bar" "bar" :parent page-1-id)) + (page-2-id (getf page-2 :id)) + (page-3 (miniblog.data:add-page "baz" "baz" "baz" :parent page-2-id)) + (page-3-id (getf page-3 :id)) + (page-4 (miniblog.data:add-page "quux" "quux" "quux" :parent page-3-id)) + (page-4-id (getf page-4 :id)) + (page-5 (miniblog.data:add-page "wibble" "wibble" "wibble" :parent page-2-id)) + (page-5-id (getf page-5 :id)) + (page-6 (miniblog.data:add-page "frotz" "frotz" "frotz" :parent page-5-id)) + (page-6-id (getf page-6 :id))) + (miniblog.data:delete-page page-2-id :children :delete) + (is (and (not (miniblog.data:get-page page-3-id))) + (not (miniblog.data:get-page page-4-id)) + (not (miniblog.data:get-page page-5-id)) + (not (miniblog.data:get-page page-6-id)))))) + +(test get-pages + "Add a tree of pages and verify that get-pages returns the tree as constructed" + (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) + (miniblog.db:init-tables) + (let* ((page-1 (miniblog.data:add-page "foo" "foo" "foo")) + (page-1-id (getf page-1 :id)) + (page-2 (miniblog.data:add-page "bar" "bar" "bar" :parent page-1-id)) + (page-2-id (getf page-2 :id)) + (page-3 (miniblog.data:add-page "baz" "baz" "baz" :parent page-2-id)) + (page-3-id (getf page-3 :id)) + (page-4 (miniblog.data:add-page "quux" "quux" "quux" :parent page-3-id)) + (page-4-id (getf page-4 :id)) + (page-5 (miniblog.data:add-page "wibble" "wibble" "wibble" :parent page-2-id)) + (page-5-id (getf page-5 :id)) + (pages (miniblog.data:get-pages)) + (maybe-page-1 (car (getf pages :children))) + (maybe-page-2 (car (getf maybe-page-1 :children))) + (maybe-page-3 (cadr (getf maybe-page-2 :children))) + (maybe-page-4 (car (getf maybe-page-3 :children))) + (maybe-page-5 (car (getf maybe-page-2 :children)))) + (is (eql page-1-id (getf maybe-page-1 :id))) + (is (eql page-2-id (getf maybe-page-2 :id))) + (is (eql page-3-id (getf maybe-page-3 :id))) + (is (eql page-4-id (getf maybe-page-4 :id))) + (is (eql page-5-id (getf maybe-page-5 :id)))))) diff --git a/tst/db.lisp b/tst/db.lisp @@ -1,217 +0,0 @@ -(in-package :miniblog/tests) - -(in-suite miniblog-test) - -(defparameter +db-settings+ '(sqlite3 :database-name ":memory:")) - -(test add-and-get-entry - "Add an entry and then fetch it back, verify all fields are correct" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((title "Test entry") - (content "Test content") - (username "test") - (new-entry (miniblog.db:add-entry title content :username username)) - (now (local-time:now)) - (new-id (getf new-entry :id)) - (fetched-entry (miniblog.db:get-entry new-id))) - (is (and (string= title (getf new-entry :title)) - (string= title (getf fetched-entry :title)))) - (is (and (string= content (getf new-entry :content)) - (string= content (getf fetched-entry :content)))) - (is (and (string= username (getf new-entry :created-by)) - (string= username (getf fetched-entry :created-by)))) - (is (and (string= username (getf new-entry :last-updated-by)) - (string= username (getf fetched-entry :last-updated-by)))) - (is (and (< (timestamp-difference now (getf new-entry :created-at)) 1) - (>= (timestamp-difference now (getf new-entry :created-at)) 0))) - (is (and (timestamp= (getf new-entry :created-at) - (getf new-entry :last-updated-at)))) - (is (and (timestamp= (getf new-entry :created-at) - (getf fetched-entry :created-at)))) - (is (and (timestamp= (getf new-entry :last-updated-at) - (getf fetched-entry :last-updated-at))))))) - -(test modify-entry - "Update an entry, verify update fields" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((title "Before title") - (content "Before content") - (username "before") - (entry (miniblog.db:add-entry title content :username username)) - (id (getf entry :id)) - (new-title "After title") - (new-content "After content") - (new-username "after") - (updated-entry (miniblog.db:update-entry id new-title new-content :username new-username))) - (is (string= new-title (getf updated-entry :title))) - (is (string= new-content (getf updated-entry :content))) - (is (string= username (getf updated-entry :created-by))) - (is (string= new-username (getf updated-entry :last-updated-by))) - (is (> (timestamp-difference (getf updated-entry :last-updated-at) (getf updated-entry :created-at)) 0))))) - -(test delete-entry - "Add an entry, delete it and verify it's removed" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((entry (miniblog.db:add-entry "foo" "foo")) - (id (getf entry :id))) - (is (miniblog.db:get-entry id)) - (miniblog.db:delete-entry id) - (is (not (miniblog.db:get-entry id)))))) - -(test add-and-get-page - "Add a page and then fetch it back, verify all fields are correct" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((name "test") - (title "Test page") - (content "Test content") - (username "test") - (new-page (miniblog.db:add-page name title content :username username)) - (now (local-time:now)) - (new-id (getf new-page :id)) - (fetched-page (miniblog.db:get-page new-id))) - (is (and (eql 0 (getf new-page :parent)) - (eql 0 (getf fetched-page :parent)))) - (is (and (string= name (getf new-page :name)) - (string= name (getf fetched-page :name)))) - (is (and (string= title (getf new-page :title)) - (string= title (getf fetched-page :title)))) - (is (and (string= content (getf new-page :content)) - (string= content (getf fetched-page :content)))) - (is (and (string= username (getf new-page :created-by)) - (string= username (getf fetched-page :created-by)))) - (is (and (string= username (getf new-page :last-updated-by)) - (string= username (getf fetched-page :last-updated-by)))) - (is (and (< (timestamp-difference now (getf new-page :created-at)) 1) - (>= (timestamp-difference now (getf new-page :created-at)) 0))) - (is (and (timestamp= (getf new-page :created-at) - (getf new-page :last-updated-at)))) - (is (and (timestamp= (getf new-page :created-at) - (getf fetched-page :created-at)))) - (is (and (timestamp= (getf new-page :last-updated-at) - (getf fetched-page :last-updated-at))))))) - -(test modify-page - "Update a page, verify update fields" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((name "before") - (title "Before title") - (content "Before content") - (username "before") - (page (miniblog.db:add-page name title content :username username)) - (id (getf page :id)) - (new-name "after") - (new-title "After title") - (new-content "After content") - (new-username "after") - (updated-page (miniblog.db:update-page id new-name new-title new-content :username new-username))) - (is (eql 0 (getf updated-page :parent))) - (is (string= new-title (getf updated-page :title))) - (is (string= new-content (getf updated-page :content))) - (is (string= username (getf updated-page :created-by))) - (is (string= new-username (getf updated-page :last-updated-by))) - (is (> (timestamp-difference (getf updated-page :last-updated-at) (getf updated-page :created-at)) 0))))) - -(test move-page - "Create several test pages, move one in the hierarchy and verify the move" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page-1 (miniblog.db:add-page "foo" "foo" "foo")) - (page-1-id (getf page-1 :id)) - (page-2 (miniblog.db:add-page "bar" "bar" "bar" :parent page-1-id)) - (page-2-id (getf page-2 :id)) - (page-3 (miniblog.db:add-page "baz" "baz" "baz" :parent page-2-id)) - (page-3-id (getf page-3 :id)) - (moved-page-2 (miniblog.db:move-page page-2-id 0)) - (page-3-post-move (miniblog.db:get-page page-3-id))) - (is (eql page-1-id (getf page-2 :parent))) - (is (eql 0 (getf moved-page-2 :parent))) - (is (and (eql page-2-id (getf page-3 :parent)) - (eql page-2-id (getf page-3-post-move :parent))))))) - -(test delete-page - "Add a page, delete it and verify it's removed" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page (miniblog.db:add-page "foo" "foo" "foo")) - (id (getf page :id))) - (is (miniblog.db:get-page id)) - (miniblog.db:delete-page id) - (is (not (miniblog.db:get-page id)))))) - -(test delete-page-moving-subtree-to-root - "Add a page with a child, delete it and verify that it's removed and its child is reparented to the root" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page-1 (miniblog.db:add-page "foo" "foo" "foo")) - (page-1-id (getf page-1 :id)) - (page-2 (miniblog.db:add-page "bar" "bar" "bar" :parent (getf page-1 :id))) - (page-2-id (getf page-2 :id))) - (miniblog.db:delete-page page-1-id :children :move-to-root) - (is (not (miniblog.db:get-page page-1-id))) - (is (eql 0 (getf (miniblog.db:get-page page-2-id) :parent)))))) - -(test delete-page-moving-subtree-to-parent - "Add three test pages, each the next one's parent, delete the second one and verify that it's removed and its child is reparented to first page" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page-1 (miniblog.db:add-page "foo" "foo" "foo")) - (page-1-id (getf page-1 :id)) - (page-2 (miniblog.db:add-page "bar" "bar" "bar" :parent page-1-id)) - (page-2-id (getf page-2 :id)) - (page-3 (miniblog.db:add-page "baz" "baz" "baz" :parent page-2-id)) - (page-3-id (getf page-3 :id))) - (miniblog.db:delete-page page-2-id) ; :MOVE-TO-PARENT is default - (is (eql page-1-id (getf (miniblog.db:get-page page-3-id) :parent)))))) - -(test delete-page-deleting-subtree - "Add six test pages with the first the second's parent and the second the top of a subtree containing the other four, delete the second one and verify that it's removed and all its children are as well" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page-1 (miniblog.db:add-page "foo" "foo" "foo")) - (page-1-id (getf page-1 :id)) - (page-2 (miniblog.db:add-page "bar" "bar" "bar" :parent page-1-id)) - (page-2-id (getf page-2 :id)) - (page-3 (miniblog.db:add-page "baz" "baz" "baz" :parent page-2-id)) - (page-3-id (getf page-3 :id)) - (page-4 (miniblog.db:add-page "quux" "quux" "quux" :parent page-3-id)) - (page-4-id (getf page-4 :id)) - (page-5 (miniblog.db:add-page "wibble" "wibble" "wibble" :parent page-2-id)) - (page-5-id (getf page-5 :id)) - (page-6 (miniblog.db:add-page "frotz" "frotz" "frotz" :parent page-5-id)) - (page-6-id (getf page-6 :id))) - (miniblog.db:delete-page page-2-id :children :delete) - (is (and (not (miniblog.db:get-page page-3-id))) - (not (miniblog.db:get-page page-4-id)) - (not (miniblog.db:get-page page-5-id)) - (not (miniblog.db:get-page page-6-id)))))) - -(test get-pages - "Add a tree of pages and verify that get-pages returns the tree as constructed" - (let ((mito:*connection* (apply #'dbi:connect +db-settings+))) - (miniblog.db:init-tables) - (let* ((page-1 (miniblog.db:add-page "foo" "foo" "foo")) - (page-1-id (getf page-1 :id)) - (page-2 (miniblog.db:add-page "bar" "bar" "bar" :parent page-1-id)) - (page-2-id (getf page-2 :id)) - (page-3 (miniblog.db:add-page "baz" "baz" "baz" :parent page-2-id)) - (page-3-id (getf page-3 :id)) - (page-4 (miniblog.db:add-page "quux" "quux" "quux" :parent page-3-id)) - (page-4-id (getf page-4 :id)) - (page-5 (miniblog.db:add-page "wibble" "wibble" "wibble" :parent page-2-id)) - (page-5-id (getf page-5 :id)) - (pages (miniblog.db:get-pages)) - (maybe-page-1 (car (getf pages :children))) - (maybe-page-2 (car (getf maybe-page-1 :children))) - (maybe-page-3 (cadr (getf maybe-page-2 :children))) - (maybe-page-4 (car (getf maybe-page-3 :children))) - (maybe-page-5 (car (getf maybe-page-2 :children)))) - (is (eql page-1-id (getf maybe-page-1 :id))) - (is (eql page-2-id (getf maybe-page-2 :id))) - (is (eql page-3-id (getf maybe-page-3 :id))) - (is (eql page-4-id (getf maybe-page-4 :id))) - (is (eql page-5-id (getf maybe-page-5 :id))))))