Revision: 3830 Author: hans URL: http://bknr.net/trac/changeset/3830
News permalink and cms functionality updates. Always scale images uploaded down to the right width.
U trunk/projects/quickhoney/src/handlers.lisp U trunk/projects/quickhoney/src/webserver.lisp U trunk/projects/quickhoney/upgrade-stuff/import.lisp U trunk/projects/quickhoney/website/static/index.css U trunk/projects/quickhoney/website/static/javascript.js U trunk/projects/quickhoney/website/templates/index.xml
Modified: trunk/projects/quickhoney/src/handlers.lisp =================================================================== --- trunk/projects/quickhoney/src/handlers.lisp 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/src/handlers.lisp 2008-09-07 14:49:19 UTC (rev 3830) @@ -38,25 +38,18 @@ (blob-to-stream (quickhoney-animation-image-animation animation) (send-headers)))))
-(defclass json-image-query-handler (object-handler quickhoney-image-handler) - ()) +(defclass json-image-info-handler (object-handler quickhoney-image-handler) + () + (:default-initargs :query-function #'store-image-with-name))
-(defparameter *editable-keywords* '(:explicit :buy-file :buy-print :buy-t-shirt) - "List of keywords that are image keywords which can be edited through the CMS") - -(defun images-in-category-sorted-by-time (cat-sub) - (sort (copy-list (images-in-category cat-sub)) - #'> :key #'blob-timestamp)) - -(defmethod object-handler-get-object ((handler json-image-query-handler)) - (images-in-category-sorted-by-time (mapcar #'make-keyword-from-string (decoded-handler-path handler)))) - (defmethod image-to-json ((image quickhoney-image)) (with-json-object () + (encode-object-element "class" (string-downcase (cl-ppcre:regex-replace "^QUICKHONEY-" (symbol-name (class-name (class-of image))) ""))) (encode-object-element "name" (store-image-name image)) - (encode-object-element "category" (quickhoney-image-category image)) - (when (quickhoney-image-subcategory image) - (encode-object-element "subcategory" (quickhoney-image-subcategory image))) + (when (quickhoney-image-category image) + (encode-object-element "category" (quickhoney-image-category image)) + (when (quickhoney-image-subcategory image) + (encode-object-element "subcategory" (quickhoney-image-subcategory image)))) (encode-object-element "id" (store-object-id image)) (encode-object-element "type" (image-content-type (blob-mime-type image))) (encode-object-element "width" (store-image-width image)) @@ -71,6 +64,24 @@ (dolist (keyword (intersection *editable-keywords* (store-image-keywords image))) (encode-object-element (string-downcase (symbol-name keyword)) t))))))
+(defmethod handle-object ((handler json-image-info-handler) image) + (with-json-response () + (with-object-element ("image") + (image-to-json image)))) + +(defclass json-image-query-handler (object-handler quickhoney-image-handler) + ()) + +(defparameter *editable-keywords* '(:explicit :buy-file :buy-print :buy-t-shirt) + "List of keywords that are image keywords which can be edited through the CMS") + +(defun images-in-category-sorted-by-time (cat-sub) + (sort (copy-list (images-in-category cat-sub)) + #'> :key #'blob-timestamp)) + +(defmethod object-handler-get-object ((handler json-image-query-handler)) + (images-in-category-sorted-by-time (mapcar #'make-keyword-from-string (decoded-handler-path handler)))) + (defmethod layout-to-json ((layout layout)) (with-json-array () (dolist (page (layout-pages layout)) @@ -252,6 +263,17 @@ (defclass upload-image-handler (admin-only-handler prefix-handler) ())
+(defun count-colors-used (&optional (image cl-gd:*default-image*)) + (let ((color-table (make-hash-table :test #'eql))) + (cl-gd:do-pixels (image) + (setf (gethash (cl-gd:raw-pixel) color-table) t)) + (hash-table-count color-table))) + +(defun maybe-convert-to-palette (&optional (image cl-gd:*default-image*)) + (when (and (cl-gd:true-color-p image) + (<= (count-colors-used image) 256)) + (cl-gd:true-color-to-palette :image image))) + (defmethod handle ((handler upload-image-handler)) (with-query-params (client spider-keywords) (let ((uploaded-file (request-uploaded-file "image-file"))) @@ -260,15 +282,10 @@ (unless uploaded-file (error "no file uploaded")) (with-image-from-upload* (uploaded-file) - (let* ((color-table (make-hash-table :test #'eql)) - (width (cl-gd:image-width)) + (let* ((width (cl-gd:image-width)) (height (cl-gd:image-height)) (ratio (/ 1 (max (/ width 300) (/ height 200))))) - (cl-gd:do-pixels () - (incf (gethash (cl-gd:raw-pixel) color-table 0))) - (when (and (cl-gd:true-color-p) - (<= (hash-table-count color-table) 256)) - (cl-gd:true-color-to-palette)) + (maybe-convert-to-palette) (let* ((image (make-store-image :name (pathname-name (upload-original-filename uploaded-file)) :class-name 'quickhoney-image :keywords (cons :upload (image-keywords-from-request-parameters)) @@ -283,8 +300,7 @@ ((:script :type "text/javascript" :language "JavaScript") "function done() { window.opener.do_query(); window.close(); }")) (:body - (:p "Image " (:princ-safe (store-image-name image)) " with " - (:princ-safe (hash-table-count color-table)) " colors uploaded") + (:p "Image " (:princ-safe (store-image-name image)) " uploaded") (:p ((:img :src (format nil "/image/~D" (store-object-id image)) :width (round (* ratio width)) :height (round (* ratio height))))) (:p ((:a :href "javascript:done()") "ok"))))))))))) @@ -303,6 +319,12 @@ (defclass upload-news-handler (admin-only-handler page-handler) ())
+(defun normalize-news-title (title) + (string-downcase (cl-ppcre:regex-replace-all "(?i)[^a-z0-9_]+" title "_"))) + +(defconstant +news-image-width+ 486 + "Width of a news image. Uploaded images that are wider are scaled down.") + (defmethod handle ((handler upload-news-handler)) (with-query-params (title text) (let ((uploaded-file (request-uploaded-file "image-file"))) @@ -310,35 +332,39 @@ (progn (unless uploaded-file (error "no file uploaded")) - (with-image-from-upload* (uploaded-file) - (let* ((color-table (make-hash-table :test #'eql)) - (width (cl-gd:image-width)) - (height (cl-gd:image-height)) - (ratio (/ 1 (max (/ width 300) (/ height 200))))) - (cl-gd:do-pixels () - (incf (gethash (cl-gd:raw-pixel) color-table 0))) - (when (and (cl-gd:true-color-p) - (<= (hash-table-count color-table) 256)) - (cl-gd:true-color-to-palette)) - (let* ((image (make-store-image :name (pathname-name (upload-original-filename uploaded-file)) - :class-name 'quickhoney-news-item - :keywords (list :upload) - :initargs (list :cat-sub (list :news) - :title title - :text text)))) - (with-http-response () - (with-http-body () - (html (:html - (:head - (:title "Upload successful") - ((:script :type "text/javascript" :language "JavaScript") - "function done() { window.opener.do_query(); window.close(); }")) - (:body - (:p "Image " (:princ-safe (store-image-name image)) " with " - (:princ-safe (hash-table-count color-table)) " colors uploaded") - (:p ((:img :src (format nil "/image/~D" (store-object-id image)) - :width (round (* ratio width)) :height (round (* ratio height))))) - (:p ((:a :href "javascript:done()") "ok"))))))))))) + (with-image-from-upload (uploaded-image uploaded-file) + (maybe-convert-to-palette uploaded-image) + (when (> (cl-gd:image-width uploaded-image) +news-image-width+) + (let* ((scaled-height (floor (* (/ +news-image-width+ (cl-gd:image-width uploaded-image)) + (cl-gd:image-height uploaded-image)))) + (scaled-image (cl-gd:create-image +news-image-width+ scaled-height (cl-gd:true-color-p uploaded-image)))) + (cl-gd:copy-image uploaded-image scaled-image + 0 0 0 0 + (cl-gd:image-width uploaded-image) (cl-gd:image-height uploaded-image) + :resample t :resize t + :dest-width +news-image-width+ :dest-height scaled-height) + (cl-gd:destroy-image uploaded-image) + (setf uploaded-image scaled-image))) + (let ((item (make-store-image :name (normalize-news-title title) + :image uploaded-image + :type (if (cl-gd:true-color-p uploaded-image) :jpg :png) + :class-name 'quickhoney-news-item + :keywords (list :upload) + :initargs (list :cat-sub (list :news) + :title title + :text text + :owner (bknr-session-user))))) + (declare (ignore item)) ; for now + (with-http-response () + (with-http-body () + (html (:html + (:head + (:title "News article created") + ((:script :type "text/javascript" :language "JavaScript") + "function done() { window.opener.reload_news(); window.close(); }")) + (:body + (:p "News article created") + (:p ((:a :href "javascript:done()") "ok")))))))))) (error (e) (with-http-response () (with-http-body () @@ -449,17 +475,18 @@ (:p (:princ-safe (apply #'format nil (simple-condition-format-control e) (simple-condition-format-arguments e)))) (:p ((:a :href "javascript:window.close()") "ok"))))))))))))
-(defclass rss-channel-handler (object-handler) +(defclass json-news-handler (object-handler) () - (:default-initargs :object-class 'rss-channel :query-function #'find-rss-channel)) + (:default-initargs :query-function (lambda (string) (or (find-rss-channel string) + (store-image-with-name string) + (find-store-object string)))))
-(defclass json-news-handler (rss-channel-handler) - ()) - (defgeneric json-encode-news-item (item) (:method ((item t)) ; do nothing ) + (:method :before ((item store-object)) + (encode-object-element "id" (store-object-id item))) (:method :before ((image quickhoney-image)) (when (owned-object-owner image) (encode-object-element "owner" (user-login (owned-object-owner image)))) @@ -480,17 +507,24 @@ (encode-object-element "width" (store-image-width item)) (encode-object-element "height" (store-image-height item))))
-(defmethod handle-object ((handler json-news-handler) (channel rss-channel)) +(defun json-encode-news-items (items) (with-json-response () (with-object-element ("items") (with-json-array () - (dolist (item (rss-channel-items channel)) + (dolist (item items) (with-json-object () (json-encode-news-item item)))))))
-(defclass json-news-archive-handler (rss-channel-handler) - ()) +(defmethod handle-object ((handler json-news-handler) (channel rss-channel)) + (json-encode-news-items (rss-channel-items channel)))
+(defmethod handle-object ((handler json-news-handler) (item quickhoney-news-item)) + (json-encode-news-items (list item))) + +(defclass json-news-archive-handler (object-handler) + () + (:default-initargs :object-class 'rss-channel :query-function #'find-rss-channel)) + (defmethod handle-object ((handler json-news-archive-handler) (channel rss-channel)) (with-json-response () (with-object-element ("months")
Modified: trunk/projects/quickhoney/src/webserver.lisp =================================================================== --- trunk/projects/quickhoney/src/webserver.lisp 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/src/webserver.lisp 2008-09-07 14:49:19 UTC (rev 3830) @@ -22,6 +22,7 @@ :handler-definitions `(("/random-image" random-image-handler) ("/animation" animation-handler) ("/json-image-query" json-image-query-handler) + ("/json-image-info" json-image-info-handler) ("/json-login" json-login-handler) ("/json-logout" json-logout-handler) ("/json-clients" json-clients-handler)
Modified: trunk/projects/quickhoney/upgrade-stuff/import.lisp =================================================================== --- trunk/projects/quickhoney/upgrade-stuff/import.lisp 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/upgrade-stuff/import.lisp 2008-09-07 14:49:19 UTC (rev 3830) @@ -68,9 +68,7 @@ (with-transaction (:initialize-news) (setf (slot-value (find-rss-channel "quickhoney") 'bknr.rss::items) (sort (remove-if (lambda (image) - (or (member :explicit (store-image-keywords image)) - (not (and (quickhoney-image-category image) - (quickhoney-image-subcategory image))))) + (member :explicit (store-image-keywords image))) (class-instances 'quickhoney-image)) #'> :key #'blob-timestamp)))
Modified: trunk/projects/quickhoney/website/static/index.css =================================================================== --- trunk/projects/quickhoney/website/static/index.css 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/website/static/index.css 2008-09-07 14:49:19 UTC (rev 3830) @@ -359,12 +359,20 @@ }
.newsentry h1 { - margin: 5px 0px 2px 0px; + margin: 10px 0px 5px 0px; font-size: 120%; font-weight: normal; color: #30be01 }
+.newsentry .delete-button { + margin: 10px 0px 5px 0px; +} + +.newsentry .item-text { + margin-top: 1em; +} + .autonews { height: 106px; color: #000; border-width: 1px; border-style: solid
Modified: trunk/projects/quickhoney/website/static/javascript.js =================================================================== --- trunk/projects/quickhoney/website/static/javascript.js 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/website/static/javascript.js 2008-09-07 14:49:19 UTC (rev 3830) @@ -247,6 +247,18 @@
function make_news_item(item, revealer) { + var delete_button = ''; + + if (logged_in) { + delete_button = BUTTON({ 'class': 'delete-button' }, "Delete"); + delete_button.onclick = function () { + if (confirm('Delete this news article?')) { + loadJSONDoc('/json-edit-image/' + item.id + '?action=delete') + .addCallbacks(reload_news, alert); + } + return false; + } + } return DIV({ 'class': 'newsentry' }, revealer.IMG({ src: "/image/" + encodeURI(item.name) + '/news-article-cutout', style: 'visibility: hidden', @@ -254,9 +266,10 @@ DIV(null, H1(null, item.title), item.date, ' by ', item.owner, ' | ', - A({ href: '#' }, 'permalink'), + A({ href: '#news/' + item.name }, 'permalink'), BR(), - item.text)); + DIV({ 'class': 'item-text' }, item.text), + delete_button)); }
function load_news(data) @@ -272,18 +285,21 @@ DIV({ 'class': 'news_sep' }) ]; }, data.items)); $('archive-navigation').style.visibility = 'inherit'; + + display_cms_window(); } catch (e) { log('error displaying news: ' + e); } }
-function news(subpath) { - var args = subpath.split('/'); - var year = args[0]; - var month = args[1]; +function reload_news() +{ + $('edit_news_form').reset(); + news(''); +}
- log('news year ' + year + ' month ' + month); +function show_news_archive_navigation(year, month) {
map(function (element) { ((element.year == year && (month || element.month)) ? addElementClass : removeElementClass) @@ -293,12 +309,33 @@ for (i = 1; i <= 12; i++) { ((month == i) ? addElementClass : removeElementClass)('archive-navigation', 'm' + i); } +}
- if (month || !year) { - show_cue('loading news'); +function news(subpath) { + + if (!subpath.match(/^[0-9/]*$/)) { + + show_cue('loading news item'); $('newsentries').style.visibility = 'hidden'; - loadJSONDoc('/json-news/quickhoney' + (month ? ('?month=' + subpath) : '')) + loadJSONDoc('/json-news/' + subpath) .addCallbacks(load_news, alert); + + } else { + + var args = subpath.split('/'); + var year = args[0]; + var month = args[1]; + + log('news year ' + year + ' month ' + month); + + show_news_archive_navigation(year, month); + + if (month || !year) { + show_cue('loading news'); + $('newsentries').style.visibility = 'hidden'; + loadJSONDoc('/json-news/quickhoney' + (month ? ('?month=' + subpath) : '')) + .addCallbacks(load_news, alert); + } } }
@@ -476,7 +513,7 @@ if (logged_in) { if (current_directory == "home") { } else if (current_directory == "news") { - show_cms_window("edit_news_form"); + show_cms_window("edit_news"); } else if (current_directory && current_subdirectory) { if (current_image) { show_cms_window('edit_form'); @@ -1160,7 +1197,7 @@ var url_path = (document.location.href + "#").split("#")[1]; if (url_path && (url_path != document.current_path)) { try { - pageTracker._trackPageview(document.location.href); + pageTracker._trackPageview(document.location.href.replace("#", "/")); } catch (e) { log("tracking failed: " + e);
Modified: trunk/projects/quickhoney/website/templates/index.xml =================================================================== --- trunk/projects/quickhoney/website/templates/index.xml 2008-09-07 10:24:06 UTC (rev 3829) +++ trunk/projects/quickhoney/website/templates/index.xml 2008-09-07 14:49:19 UTC (rev 3830) @@ -295,9 +295,9 @@ </form> </div>
- <div id="edit_news_form" class="cms_form"> + <div id="edit_news" class="cms_form"> <div class="cms_title">Create news entry</div> - <form action="/upload-news" method="post" + <form action="/upload-news" method="post" id="edit_news_form" enctype="multipart/form-data" target="upload_result" onsubmit="do_upload(this.target);"> <p class="cms"> <table> @@ -321,10 +321,6 @@ <input type="submit" name="action" value="upload" /> </p> </form> - <div class="cms_title">Delete this news entry</div> - <form id="delete_newsentry_form_element" action="/edit-news-js" target="edit_iframe" method="post"> - <input type="submit" name="action" value="delete" onclick="return confirm('Really delete this news entry?');" /> - </form> </div>
<div id="saving_edits_form" class="cms_form">