|
(cl:defpackage :trail (:use :cl :araneida :webutils :ironclad :colorize) (:export :start-trail)) (cl:in-package :trail) (defvar *url-root* (parse-urlstring "http://trail.unmutual.info/")) (defvar *password* #(66 11 109 79 204 29 245 128 154 53 68 144 145 40 70 147 54 220 97 69)) (defvar *listener* #+single-http-listener single-http-listener:*listener* #-single-http-listener (make-instance 'serve-event-reverse-proxy-listener :translations `((,(urlstring *url-root*) (:wild-host "/"))) :address #(0 0 0 0) :port 8080)) (define-application trail *listener*) (define-configuration-variable *pathname-root* (make-pathname :directory (pathname-directory (load-time-value *load-truename*)))) (defvar *projects* nil) (define-simple-serialized-class project () ((name :initarg :name :accessor project-name) (tasks :initform nil :initarg :tasks :accessor project-tasks) (private-p :initform nil :initarg :private-p :accessor project-private-p)) (:key-slot name) (:store *projects*) (:store-directory (merge-pathnames (make-pathname :directory '(:relative "projects")) *pathname-root*)) (:managed-slots tasks)) (defvar *tasks* nil) (defvar *task-id* 0) (define-simple-serialized-class task () ((id :initarg :id :accessor task-id) (project :initarg :project :accessor task-project) (open-p :initarg :open-p :accessor task-open-p) (title :initarg :title :accessor task-title) (description :initarg :description :accessor task-description) (time :initarg :time :accessor task-time) (estimate :initarg :estimate :accessor task-estimate) (commits :initarg :commits :initform nil :accessor task-commits)) (:key-slot id) (:store *tasks*) (:store-directory (merge-pathnames (make-pathname :directory '(:relative "tasks")) *pathname-root*))) (defmethod register-key (key (class (eql 'task))) (setf *task-id* (max *task-id* key))) (define-object-relations task (task-project project-tasks :type :one-to-many)) (defclass login-mixin () ()) (defclass authentication-mixin () ()) (define-handler-hierarchy (:application trail) (*url-root* ("" (main-handler login-mixin)) ("login" login-handler) ("logout" logout-handler) ("view-project" (view-project-handler login-mixin) :inexact t) ("new-project" (new-project-handler authentication-mixin)) ("edit-project" (edit-project-handler authentication-mixin) :inexact t) ("view-task" (view-task-handler login-mixin) :inexact t) ("new-task" (new-task-handler authentication-mixin)) ("edit-task" (edit-task-handler authentication-mixin) :inexact t) ("adjust-task-time" (adjust-task-time-handler authentication-mixin)) ("adjust-task-estimate" (adjust-task-estimate-handler authentication-mixin)) ("add-commit" (add-commit-handler authentication-mixin)) ("set-task-open" (set-task-open-handler authentication-mixin)) ("view-source" view-source-handler))) (install-handler (http-listener-handler *listener*) (make-instance 'static-file-handler :pathname (merge-pathnames (make-pathname :directory '(:relative "static")) *pathname-root*)) (urlstring (merge-url *url-root* "static/")) nil) (defvar *is-authorized* nil) (defun main-menu () (with-xml (flet ((link (handler text) (let ((url (urlstring (handler-url handler)))) (<li> (<a href=?url> text))))) (<ul> (link 'main-handler "Main") (if *is-authorized* (link 'logout-handler "Logout") (link 'login-handler "Login")) (link 'new-project-handler "New Project") (link 'new-task-handler "New Task"))))) (defmethod application-wrap-page ((application trail) request title body &rest extra-headers) (apply #'request-send-headers request :expires 0 extra-headers) (with-xml-output-to-stream (request-stream request) (<html> (<head> (<title> title) (let ((css-url (urlstring (merge-url *url-root* "static/trail.css")))) <link rel= "stylesheet" type= "text/css" href=?css-url />)) (<body> (<div class= "menu"> (main-menu)) (<div id= "body"> body) (<div class= "menu"> (<ul> (<li> (let ((url (urlstring (handler-url 'view-source-handler)))) (<a href=?url> "View Trail's Source"))))))))) (define-application-handler :around ((handler login-mixin) t request) (if (is-authorized request) (let ((*is-authorized* t)) (call-next-method)) (call-next-method))) (define-application-handler :around ((handler authentication-mixin) t request) (if (is-authorized request) (let ((*is-authorized* t)) (call-next-method)) (application-page ("Please login") (let ((url (urlstring (handler-url 'login-handler)))) (<a href=?url> "Login"))))) (define-form login () (password) (:semantic-check (if (not (equalp password *password*)) (fail-check "Please enter your password correctly." password)))) (define-form-field (login password) password-form-field :string-to-value-translator (lambda (string) (coerce (digest-sequence :ripemd-160 (map '(vector (unsigned-byte 8)) #'char-code string)) '(vector t))) :value-to-string-translator (constantly "")) (define-application-handler ((handler login-handler) :get request) (application-page ("Login") (form-html (login (handler-url 'login-handler))))) (define-application-handler ((handler login-handler) :post request) (expire-authorization-tokens :filter (constantly t)) (application-process-form (login) :success (let ((*is-authorized* t)) (application-page ("Login" handler request :set-cookie (make-authorization-token)) "You've logged in successfully." <p/> (let ((url (urlstring (handler-url 'main-handler)))) (<a href=?url> "Home.")))))) (define-application-handler ((handler logout-handler) t request) (expire-authorization-tokens :filter (constantly t)) (application-page ("Logout") "You've logged out.")) (define-table-generator format-tasks (:id #'task-link #'task-id) (:open (lambda (task) (if (task-open-p task) "Yes" "No"))) (:title #'task-title) (:time #'task-time) (:estimate #'task-estimate) (:commits (lambda (task) (format nil "~{~A~^ ~}" (task-commits task))))) (define-extended-table-generator format-task (:basic (:id #'task-link) (:title #'task-title) (:project #'(lambda (task) (project-link (task-project task))))) (:description (:description #'task-description)) (:status (:open (lambda (task) (if (task-open-p task) "Yes" "No"))) (:time (lambda (task) (princ-to-string (task-time task)))) (:estimate (lambda (task) (princ-to-string (task-estimate task)))) (:commits (lambda (task) (format nil "~{~A~^ ~}" (task-commits task)))))) (define-form project () ((project :select :edit) (name :create) (private-p :create :edit))) (define-class-gate-form-field (project project :select) selector-form-field project) (define-class-gate-form-field (project project :edit) hidden-form-field project) (define-form-field (project name :create) form-field :string-acceptor (lambda (string) (if (zerop (length string)) (fail-check "Please enter a project name.")) (if (find-instance-by-key string 'project) (fail-check "There already is a project with this name.")))) (define-form-field (project private-p :create :edit) boolean-form-field :pretty-name "Is Private?") (define-form view-project (format-tasks) (project only-open) (:semantic-check (if (and (project-private-p project) (not *is-authorized*)) (fail-check "This project is private. Log in first.")))) (defun project-link (project &key (text (project-name project))) (let ((url (form-get-url (view-project (handler-url 'view-project-handler)) (project project) (only-open t)))) (with-xml (<a href=?url> text)))) (define-class-gate-form-field (view-project project) selector-form-field project) (define-form-field (view-project only-open) boolean-form-field :pretty-name "Show Only Open Tasks?") (define-form task () ((task :select :hidden-select :edit) (project :create) (open-p :edit) (title :create :edit) (description :create :edit) (time :edit) (estimate :create :edit)) (:semantic-check :select :hidden-select :edit (if (and (project-private-p (task-project task)) (not *is-authorized*)) (fail-check "This project is private. Log in first.")))) (defun task-link (task &key (text (prin1-to-string (task-id task)))) (let ((url (form-get-url ((task :select) (handler-url 'view-task-handler)) (task task)))) (with-xml (<a href=?url> text)))) (define-class-gate-form-field (task task :select) form-field task :key-slot-type integer) (define-class-gate-form-field (task task :hidden-select :edit) hidden-form-field task :key-slot-type integer) (define-class-gate-form-field (task project :create) selector-form-field project) (define-form-field (task open-p :edit) boolean-form-field) (define-form-field (task title :create :edit) form-field :string-acceptor #'nonempty-string-validator) (define-form-field (task description :create :edit) textarea-form-field) (define-template-form-field integer-field form-field :string-acceptor (lambda (string) (unless (parse-integer string :junk-allowed t) (fail-check "Please enter an integer."))) :string-to-value-translator #'parse-integer :value-to-string-translator #'prin1-to-string) (define-form-field (task time :edit) integer-field) (define-form-field (task estimate :create :edit) integer-field) (define-form adjust-time () (task delta)) (define-class-gate-form-field (adjust-time task) hidden-form-field task :key-slot-type integer) (define-form-field (adjust-time delta) integer-field) (define-form set-open () (task open) (:semantic-check (unless open (when (and (not (zerop (task-estimate task))) (zerop (task-time task))) (fail-check "Please log at least some time to this task.")) (unless (task-commits task) (fail-check "Please log at least one commit to this task."))))) (define-class-gate-form-field (set-open task) hidden-form-field task :key-slot-type integer) (define-form-field (set-open open) boolean-form-field) (define-form add-commit () (task commit)) (define-class-gate-form-field (add-commit task) hidden-form-field task :key-slot-type integer) (define-form-field (add-commit commit) integer-field) (define-application-handler ((handler main-handler) t request) (application-page ("Main") (<h1> "View a project") (form-html (view-project (handler-url 'view-project-handler) :method :get) (only-open t)) (<h1> "View a task") (form-html ((task :select) (handler-url 'view-task-handler) :method :get)))) (define-application-handler ((handler view-project-handler) :get request) (application-process-form (view-project) (application-page ("View Project") (format-tasks (if only-open (remove-if-not #'task-open-p (project-tasks project)) (project-tasks project)) request) <p/> (resubmit-form (handler-url 'view-project-handler) :method :get) <p/> (let ((url (form-get-url ((project :select) (handler-url 'edit-project-handler)) (project project)))) (<a href=?url> "Edit Project"))))) (define-application-handler ((handler new-project-handler) :get request) (application-page ("New Project") (form-html ((project :create) (handler-url 'new-project-handler))))) (define-application-handler ((handler new-project-handler) :post request) (application-process-form ((project :create)) (register-instance (make-instance 'project :name name :private-p private-p)) (application-page ("New Project") "It's been created."))) (define-application-handler ((handler edit-project-handler) :get request) (application-process-form ((project :select)) (application-page ("Edit Project") (form-html ((project :edit) (handler-url 'edit-project-handler) :gate-fields project) (project project))))) (define-application-handler ((handler edit-project-handler) :post request) (application-process-form ((project :edit)) (serialize-instance (set-slots-from-form-values project)) (application-page ("Edit Project") "It's been done."))) (define-application-handler ((handler new-task-handler) :get request) (application-page ("New Task") (form-html ((task :create) (handler-url 'new-task-handler)) (estimate 0)))) (define-application-handler ((handler new-task-handler) :post request) (application-process-form ((task :create)) (let ((task (make-instance 'task :id (incf *task-id*) :project project :open-p t :title title :description description :time 0 :estimate estimate :commits nil))) (register-instance task) (application-page ("New Task") "The task has been made." <p/> (task-link task :text "View the task.") <p/> (project-link project :text "View the project."))))) (define-application-handler ((handler view-task-handler) :get request) (application-process-form ((task :select)) (application-page ("View Task") (format-task task) <p/> (form-html ((task :hidden-select) (handler-url 'edit-task-handler) :method :get :submit-text "Edit Task") (task task)) <p/> (form-html (adjust-time (handler-url 'adjust-task-time-handler) :submit-text "Adjust Time") (task task) (delta 0)) <p/> (form-html (adjust-time (handler-url 'adjust-task-estimate-handler) :submit-text "Adjust Estimate") (task task) (delta 0)) <p/> (form-html (set-open (handler-url 'set-task-open-handler) :submit-text (if (task-open-p task) "Close Task" "Open Task")) (task task) (open (not (task-open-p task)))) <p/> (form-html (add-commit (handler-url 'add-commit-handler) :submit-text "Add Commit") (task task))))) (define-application-handler ((handler edit-task-handler) :get request) (application-process-form ((task :select)) (application-page ("Edit Task") (form-html ((task :edit) (handler-url 'edit-task-handler) :gate-fields task) (task task))))) (define-application-handler ((handler edit-task-handler) :post request) (application-process-form ((task :edit)) (serialize-instance (set-slots-from-form-values task)) (application-page ("Edit Task") "It's been done." <p/> (task-link task :text "Go back to the task page.")))) (define-application-handler ((handler adjust-task-time-handler) :post request) (application-process-form (adjust-time) (incf (task-time task) delta) (serialize-instance task) (application-page ("Adjust Task Time") "It's been done." <p/> (task-link task :text "Go back to the task page.")))) (define-application-handler ((handler adjust-task-estimate-handler) :post request) (application-process-form (adjust-time) (incf (task-estimate task) delta) (serialize-instance task) (application-page ("Adjust Task Estimate") "It's been done." <p/> (task-link task :text "Go back to the task page.")))) (define-application-handler ((handler set-task-open-handler) :post request) (application-process-form (set-open) (setf (task-open-p task) open) (serialize-instance task) (application-page ("Set Open") "It's been done." <p/> (task-link task :text "Go back to the task page.")))) (define-application-handler ((handler add-commit-handler) :post request) (application-process-form (add-commit) (push commit (task-commits task)) (serialize-instance task) (application-page ("Add Commit") "It's been done." <p/> (task-link task :text "Go back to the task page.")))) (defparameter *memoized-time* 0) (defvar *memoized*) (defmethod handle-request-response ((handler view-source-handler) (method (eql :get)) request) (let ((pathname '#.*compile-file-truename*)) (when (> (file-write-date pathname) *memoized-time*) (setf *memoized-time* (get-universal-time)) (setf *memoized* (with-output-to-string (s nil :element-type 'base-char) (colorize-file-to-stream :common-lisp pathname s))))) (request-send-headers request :expires 0) (write-string *memoized* (request-stream request)) t) (defmethod start-application ((application trail)) #-single-http-listener (start-listening *listener*) (load-store-for-class 'project) (load-store-for-class 'task)) |