;;;; blog-v4: add a blog widget ;;;; One straightforward way to go is to have the BLOG-WIDGET contain ;;;; a list of POST-WIDGETs for each post, and one for the current ;;;; post. The weblocks COMPOSITE widget can already handle a list of ;;;; widgets for us, so we'll use it again. ;;; src/specials.lisp (in-package :blog) (defvar *blog-title* "Blog") ;;; src/widgets/blog.lisp (in-package :blog) (defwidget blog-widget () ((current-post :accessor current-post :initarg :current-post :initform nil :documentation "POST-WIDGET containing the current post when the blog is in :POST mode") (posts :accessor posts :initarg :posts :initform (make-instance 'composite) :documentation "composite widget that contains a POST-WIDGET for each post of the blog") (mode :accessor mode :initarg :mode :initform :blog :documentation "The blog can be in two modes, :BLOG and :POST. In :BLOG mode to display a list of posts, and in :POST mode to display an individual post.") (post-short-view :accessor post-short-view :initarg :post-short-view :initform nil :documentation "see SHORT-VIEW slot of POST-WIDGET") (post-full-view :accessor post-full-view :initarg :post-full-view :initform nil :documentation "see FULL-VIEW slot of POST-WIDGET")) (:documentation "widget to handle a blog")) (defgeneric blog-action-blog-mode (blog-widget) (:documentation "return an action that will switch BLOG-WIDGET into :BLOG mode") (:method ((blog-widget blog-widget)) (make-action (lambda (&rest args) (declare (ignore args)) (when (current-post blog-widget) (setf (mode (current-post blog-widget)) :short)) (setf (mode blog-widget) :blog) (reset-blog blog-widget))))) (defgeneric blog-make-post-widget (blog-widget post) (:documentation "make a POST-WIDGET containing POST. (called by RESET-BLOG)") (:method ((blog-widget blog-widget) (post post)) (make-instance 'post-widget :post post :short-view (post-short-view blog-widget) :full-view (post-full-view blog-widget) ;;; we'll add a new slot to POST-WIDGET for this :on-select (lambda (post-widget) (setf (current-post blog-widget) post-widget) (setf (mode blog-widget) :post))))) (defgeneric reset-blog (blog-widget) (:documentation "Reset the list of post widgets from the posts in the database. This function is called by BLOG-ACTION-BLOG-MODE.") (:method ((blog-widget blog-widget)) (setf (composite-widgets (posts blog-widget)) (mapcar (lambda (post) (blog-make-post-widget blog-widget post)) (all-posts))))) (defmethod initialize-instance :after ((obj blog-widget) &key) (reset-blog obj)) (defgeneric render-blog (blog-widget mode) (:documentation "render a blog widget in mode MODE. This function is called by RENDER-WIDGET-BODY.")) (defmethod render-blog ((blog-widget blog-widget) (mode (eql :blog))) (with-html (:h1 *blog-title*)) (render-widget (posts blog-widget))) (defmethod render-blog ((blog-widget blog-widget) (mode (eql :post))) (with-html (:h1 ;; link to come back to the blog (render-link (blog-action-blog-mode blog-widget) *blog-title*))) (render-widget (current-post blog-widget))) (defmethod render-widget-body ((obj blog-widget) &key) (render-blog obj (mode obj))) ;;;; When we make the POST-WIDGET in the function ;;;; BLOG-MAKE-POST-WIDGET, we introduced a new :ON-SELECT initarg ;;;; which is a function that should be called when the post is ;;;; selected (to view it) to appropriately set the state of ;;;; BLOG-WIDGET. So we'll now make the corresponding changes to ;;;; POST-WIDGET. ;;; src/widgets/post.lisp (in-package :blog) (defwidget post-widget () (;; slots as before (post :accessor post :initarg :post :initform nil) (mode :accessor mode :initarg :mode :initform :short :documentation "The post can be displayed in two versions, :SHORT and :FULL.") (short-view :accessor short-view :initarg :short-view :initform nil :documentation "View to determine how the post is displayed when in :SHORT mode.") (full-view :accessor full-view :initarg :full-view :initform nil :documentation "View to determine how the post is displayed when in :SHORT mode.") ;; new slot (on-select :accessor on-select :initarg :on-select :initform nil :documentation "Function to be called when this post is selected. It accepts POST-WIDGET as argument.")) (:documentation "widget to handle a blog post")) ;;; new function (defgeneric post-action-select (post-widget) (:documentation "return an action that selects POST-WIDGET") (:method ((post-widget post-widget)) (make-action (lambda (&rest args) (declare (ignore args)) (setf (mode post-widget) :full) (safe-funcall (on-select post-widget) post-widget))))) (defmethod render-widget-body ((obj post-widget) &key) (ecase (mode obj) (:short (when (short-view obj) (render-object-view (post obj) (short-view obj) :widget obj ;; after the fields of the POST object, ;; display a link to see the full post :fields-suffix-fn (lambda (&rest args) (declare (ignore args)) (when (on-select obj) (render-link (post-action-select obj) "more")))))) (:full (when (full-view obj) (render-object-view (post obj) (full-view obj) :widget obj))))) ;;;; Now we'll change the MAKE-BLOG-WIDGET function in the layout to ;;;; make a BLOG-WIDGET instead of a POST-WIDGET. And provide two ;;;; specialized views POST-SHORT-VIEW and POST-FULL-VIEW to nicely ;;;; display the posts. ;;; modification in src/layout.lisp (defun make-blog-widget () (let ((composite (make-instance 'composite :widgets (list (make-instance 'blog-widget :post-short-view 'post-short-view :post-full-view 'post-full-view))))) (push (lambda () (render-link (lambda (&rest args) (declare (ignore args)) (answer composite)) "admin")) (composite-widgets composite)) composite)) ;;;; For the views, we'll have POST-DATA-VIEW display properly ;;;; formatted time and the author name instead of "User" that is the ;;;; default when converting an object of class USER to a string. We ;;;; can then inherit from this view and hide either the TEXT slot or ;;;; the SHORT-TEXT slot to define the short and full views. ;;; modification in src/views.lisp (defview post-data-view (:type data :inherit-from '(:scaffold post)) (author :reader #'post-author-name) (time :reader #'post-formatted-time)) (defview post-short-view (:type data :inherit-from 'post-data-view) (text :hidep t)) (defview post-full-view (:type data :inherit-from 'post-data-view) (short-text :hidep t)) ;;; add in src/models.lisp (defgeneric post-author-name (post) (:method ((post post)) (when (post-author post) (user-name (post-author post))))) (defun post-formatted-time (post) (multiple-value-bind (second minute hour date month year day daylight-p zone) (decode-universal-time (post-time post)) (declare (ignore second daylight-p zone date)) ;; my format-foo is not very good so please improve if you can :) (format nil "~d-~d-~d ~d:~d" year month day hour minute))) ;;;; ChangeLog blog-v4 * blog.asd (blog): add new file * src/models.lisp (post-author-name, post-formatted-time): backend functions * src/layout.lisp (make-blog-widget): make a BLOG-WIDGET instead of POST-WIDGET (make-blog-widget): use new views for the post * src/views.lisp (post-data-view): modify to display formatted time, and user name instead of "User" (post-short-view, post-full-view): new views for used the two states POST-WIDGET * src/widgets/post.lisp (post-widget): add ON-SELECT slot so that BLOG-WIDGET can set a call-back (post-action-select): return an action that selects POST-WIDGET (render-widget-body): modify to add a link to see the full post [and call the ON-SELECT function if defined] * src/widgets/blog.lisp: (blog-action-blog-mode, blog-make-post-widget, reset-blog) (render-blog, initialize-instance, render-widget-body): new blog widget * src/specials.lisp (*blog-title*): blog title blog-v3 * blog.asd (blog): updated for new files * src/layout.lisp (make-blog-widget): create a composite widget with a post widget and a link (make-admin-page): add a link to MAKE-BLOG-WIDGET * src/models.lisp (all-posts, post-by-id): backend functions * src/widgets/post.lisp (post-widget): simple post widget (render-widget-body): specialized method to render the post blog-v2 * src/models.lisp (post-author-id, all-users): functions used by the views * src/views.lisp (post-form-view): override some fields - textarea for the texts, and dropdown list for the author blog-v1: * src/views.lisp (user-table-view, user-data-view, user-form-view) (post-table-view, post-data-view, post-form-view): scaffolded views for the gridedit interface * src/init-session.lisp (init-user-session): call MAKE-ADMIN-PAGE * src/layout.lisp (make-users-gridedit, make-posts-gridedit) (make-admin-page): add simple gridedit interface for the two models * src/models.lisp (user, post): USER and POST models