Tagged as LISP, Programming
Written on 2010-11-22 04:24:03
Disclaimer: What? You haven't already read the first two parts? Feel free to go ahead and do that. The same disclaimers apply. (require 'cl-wdp-part1 'cl-wdp-part2)
(in-package :clockwork)
(defview reminder-form-view (:type form :caption "Schedule an Event Reminder..."
:buttons '((:submit . "Submit")) :persistp nil)
(send-as :present-as (dropdown :choices '(("An email and a text." . :both)
("Just an e-mail." . :email)
("Just a text." . :text))
:welcome-name "How to send it")
:requiredp t)
(email :satisfies 'valid-email)
(cell-number :satisfies 'valid-cell-number)
(cell-carrier :present-as (dropdown :choices *sms-gateways*))
(event-date :present-as (calendar) :requiredp t)
(event-hour :present-as (dropdown :choices *hour-choices*)
:requiredp t)
(event-minute :present-as (dropdown :choices '(("00" . 0)
("15" . 15)
("30" . 30)
("45" . 45)))
:requiredp t)
(timezone :present-as (dropdown :choices *timezones*)
:requiredp t)
(remind-me :present-as (dropdown :choices '(("At the event" . 0)
("5 minutes before" . 300)
("10 minutes before" . 600)
("15 minutes before" . 900)
("30 minutes before" . 1800)
("45 minutes before" . 2700)
("1 hour before" . 3600)
("2 hours before" . 7200)
("1 day before" . 86400)
("2 days before" . 172800)
("1 week before" . 604800)
("2 weeks before" . 1209600)))
:requiredp t)
(subject :requiredp t)
(summary :present-as (textarea :rows 5))
(honeypot :label "Leave this blank" :satisfies #'null))
(defparameter *timezones*
'(("UTC-12:00 (Eniwetok, Kwajalein)" . -43200)
("UTC-11:00 (Midway Island, Samoa)" . -39600)
("UTC-10:00 (Hawaii)" . -36000)
("UTC-09:00 (Alaska)" . -32400)
("UTC-08:00 (Pacific Time)" . -28800)
("UTC-07:00 (Mountain Time)" . -25200)
("UTC-06:00 (Central Time)" . -21600)
("UTC-05:00 (Eastern Time)" . -18000)
("UTC-04:00 (Atlantic Time, Caracas)" . -14400)
("UTC-03:30 (Newfoundland)" . -12600)
("UTC-03:00 (Brazil, Buenos Aires, Georgetown)" . -10800)
("UTC-02:00 (Mid-Atlantic)" . -7200)
("UTC-01:00 (Azores, Cape Verde Islands)" . -3600)
("UTC+00:00 (Lisbon, London, Casablanca)" . 0)
("UTC+01:00 (Berlin, Brussels, Copenhagen, Madrid, Paris)" . 3600)
("UTC+02:00 (Kaliningrad, South Africa)" . 7200)
("UTC+03:00 (Baghdad, Moscow, Riyadh, St. Petersburg)" . 10800)
("UTC+03:30 (Tehran)" . 12600)
("UTC+04:00 (Abu Dhabi, Baku, Muscat, Tbilisi)" . 14400)
("UTC+04:30 (Kabul)" . 16200)
("UTC+05:00 (Ekaterinburg, Islamabad, Karachi, Tashkent)" . 18000)
("UTC+05:30 (Bombay, Calcutta, Madras, New Delhi)" . 19800)
("UTC+05:45 (Kathmandu)" . 20700)
("UTC+06:00 (Almaty, Colombo, Dhaka)" . 21600)
("UTC+07:00 (Bangkok, Hanoi, Jakarta)" . 25200)
("UTC+08:00 (Beijing, Hong Kong, Perth, Singapore)" . 28800)
("UTC+09:00 (Osaka, Seoul, Sapporo, Tokyo, Yakutsk)" . 32400)
("UTC+09:30 (Adelaide, Darwin)" . 34200)
("UTC+10:00 (Eastern Australia, Guam, Vladivostok)" . 36000)
("UTC+11:00 (Magadan, New Caledonia, Solomon Islands)" . 39600)
("UTC+12:00 (Auckland, Fiji, Kamchatka, Wellington)". 43200)))
(defparameter *hour-choices*
(loop for i from 0 to 23
collecting `(,(format nil "~d" i) . ,i)))
(defun valid-email (user-input)
"Ensure that there is an @ and a . and input not containing @s before and after each."
(or (cl-ppcre:scan "^[^@]+@[^@]+\\.[^@]+$" user-input)
(values nil "Your email must have an @, a . and text before and after both.")))
(defun valid-cell-number (user-input)
"Ensure that only numbers are given and there are at least 10."
(or (cl-ppcre:scan "^[0-9]{10,}$" user-input)
(values nil "Your number must have only numbers and at least 10 of them.")))
(defun get-emails (form-data)
(with-form-values (send-as email cell-number cell-carrier) form-data
(let ((sms-mail (concatenate 'string cell-number "@" cell-carrier)))
;; this was an ecase with keywords but weblocks converts
;; the keywords to strings somewhere in form submission
(cond ((string= send-as "BOTH") (list email sms-mail))
((string= send-as "EMAIL") (list email))
((string= send-as "TEXT") (list sms-mail))))))
(defun get-timestamps (form-data)
(with-form-values (event-date event-hour event-minute
remind-me timezone) form-data
(let* ((hour (parse-integer event-hour))
(minute (parse-integer event-minute))
(reminder-time-period (parse-integer remind-me))
(timezone (parse-integer timezone))
(datestring (split-sequence #\- event-date))
(day (parse-integer (first datestring)))
(month (parse-integer (second datestring)))
(year (parse-integer (third datestring)))
(event-time (encode-timestamp 0 0 minute hour day month year :offset timezone)))
(list event-time
(timestamp- event-time reminder-time-period :sec)))))
(wop:make-app 'name "/path/to/app")
weblocks generates a defwebapp form and a basic package for that app along with setting up a store and some basic resources. To include the jQuery code on our page we'll modify our defwebapp form in clockwork.lisp like so:
(defwebapp clockwork
:prefix "/"
:description "Fire-and-Forget Event Reminders"
:init-user-session 'clockwork::init-user-session
:autostart nil ;; have to start the app manually
:ignore-default-dependencies nil ;; accept the defaults
:hostnames '("clockwork.redlinernotes.com")
:dependencies '((:stylesheet "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/themes/ui-darkness/jquery-ui.css")
(:script "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js")
(:script "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.min.js")
(:javascript-code "var $jquery = jQuery.noConflict();"))
:debug t)
jQuery.noConflict()
in the header because Weblocks uses prototype and scriptaculous out of the box and jQuery will happily steal the $ global variable from Prototype which causes all sorts of havoc. While there is interest in removing the prototype and scriptaculous dependencies it hasn't happened yet. It would be greatly appreciated if any developer felt like taking a little time to tackle this.:present-as (calendar)
to a slot in our View and have it render as a calendar. This presentation will be a little different as we're using Leslie's new form-widget code. A more traditional coverage of presentations can be found in this blog post. Create a new file in the src directory called calendar.lisp and insert the following code:
(in-package :clockwork)
;; calendar presentation
(defclass calendar-presentation (input-presentation)
())
;; calendar form-widget code
(define-widget calendar-field-widget (field-widget)
()
(:default-initargs :parser (lambda (raw-value)
(values t raw-value))))
(defmethod field-presentation->field-widget-class ((presentation calendar-presentation))
'calendar-field-widget)
(defmethod render-field-contents ((form form-widget) (field calendar-field-widget))
(with-html
(:input :type "hidden" :name (name-of field) :value (datestring))
(:div :id "datepicker"
(send-script '($jquery (lambda ()
(ps:chain ($jquery "#datepicker")
(datepicker (ps:create date-format "dd-mm-yy"
min-date 0
on-select (lambda (date inst)
(ps:chain ($jquery "[name=event-date]")
(val date))))))))))))
(defun datestring ()
(subseq (format-timestring nil (now) :format '((:day 2) "-" (:month 2) "-" :year)) 0 10))
(defun init-user-session (root)
(setf (widget-children root)
(make-reminder-form)))
(defun make-reminder-form ()
(let ((reminder-form (make-instance 'form-widget :on-success 'submit-reminder-form)))
(form-widget-initialize-from-view reminder-form 'reminder-form-view)
reminder-form))
(make-reminder-form)
to create our form instance here rather than defining it inline in the init-user-session code. Make-reminder-form itself creates an instance of the form-widget class which runs a function called submit-reminder-form when the form is successfully submitted (i.e. passes validation, etc). Note that we have not yet defined submit-reminder-form. Because the form is really based on a view and not a widget or class we want to persist we'll use form-widget-initialize-from-view in conjunction with the reminder-form-view we defined earlier. Note that you may need to restart the webapp after redefining the init-user-session function. Run (restart-webapp 'clockwork)
and check the homepage. You should now have a nice form complete with jQuery Datepicker. But of course, nothing useful happens on submission. Time to fix that by going ahead and defining submit-reminder-form. Open src/init-session.lisp back up and insert the following:
(defun submit-reminder-form (widget)
(let ((new-reminder (create-reminder widget)))
(schedule new-reminder)
(persist-object *clockwork-store* new-reminder))
(reset-form-widget widget))
(defun create-reminder (form-data)
(with-form-values (subject summary) form-data
(let ((timestamps (get-timestamps form-data)))
(make-instance 'reminder
:emails (get-emails form-data)
:title subject
:summary summary
:timestamp (first timestamps)
:at (second timestamps)))))
(defun recover-reminders ()
"A function to reschedule reminders after a reboot. Based on testing,
any that expired during the reboot will be sent when the schedule method is called.
Better late than never, right?"
(mapcar #'schedule (find-persistent-objects *clockwork-store* 'reminder)))
(recover-reminders)
will schedule all the reminders in the store and whether Linux, SBCL or trivial-timers is to thank, a timer that's scheduled in the past will trigger immediately so you don't have to worry about some being lost during the reboot itself. Just add #:recover-reminders
to the export list in the clockwork defpackage in clockwork.lisp and then call it after you load clockwork in your init.lisp file that runs when the server starts. Here's my init.lisp as an example.