aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Tom Willemse2013-07-29 02:09:34 +0200
committerGravatar Tom Willemse2013-07-29 02:09:34 +0200
commit781befeb23bdbe90f1a8220da86d39ab6fd012fb (patch)
tree131c761e45481d592232b6c0bfef5a581edd8cb5
parent0b13436d1a0907f6b389b5a32f5b078aa12f11b0 (diff)
downloadscrumli-781befeb23bdbe90f1a8220da86d39ab6fd012fb.tar.gz
scrumli-781befeb23bdbe90f1a8220da86d39ab6fd012fb.zip
Rewrite with ningle
Replaces restas with ningle. Restas had 2 problem I could not overcome: 1) It would only let me return a status code or a response, not, for example, a 403 status code with some json. 2) It would not allow me to place it under a subdirectory. Both of these problems possibly (likely) have solutions with restas, but I already found out how to do these things with ningle. This rewrite is sloppy and messy. The code should be cleaned up soon.
-rw-r--r--data.lisp115
-rw-r--r--defmodule.lisp66
-rw-r--r--packages.lisp25
-rw-r--r--pg-datastore.lisp136
-rw-r--r--scrumli.asd15
-rw-r--r--scrumli.lisp363
-rwxr-xr-xstart22
-rw-r--r--static/js/main.js30
-rw-r--r--util.lisp8
9 files changed, 371 insertions, 409 deletions
diff --git a/data.lisp b/data.lisp
new file mode 100644
index 0000000..a9edc27
--- /dev/null
+++ b/data.lisp
@@ -0,0 +1,115 @@
+;; scrumli --- A simple scrum web application
+;; Copyright (C) 2013 Tom Willemse
+
+;; scrumli is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; scrumli is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with scrumli. If not, see <http://www.gnu.org/licenses/>.
+
+(in-package :scrumli)
+
+(defclass story ()
+ ((id :col-type serial :reader story-id)
+ (state :col-type string :reader state :initform "TODO")
+ (role :col-type string :reader role :initarg :role)
+ (necessity :col-type string :reader necessity :initarg :necessity)
+ (title :col-type string :reader title :initarg :title)
+ (priority :col-type integer :reader priority :initarg :priority)
+ (content :col-type string :reader content :initarg :content)
+ (reporter :col-type string :reader reporter :initarg :reporter)
+ (assignee :col-type string :reader assignee :initarg :assignee))
+ (:metaclass dao-class)
+ (:keys id))
+
+(defclass task ()
+ ((id :col-type serial :reader story-id)
+ (state :col-type string :reader state :initform "TODO")
+ (description :col-type string :reader description :initarg :description)
+ (priority :col-type integer :reader priority :initarg :priority)
+ (reporter :col-type string :reader reporter :initarg :reporter)
+ (assignee :col-type string :reader assignee :initarg :assignee)
+ (story-id :col-type integer :reader story-id :initarg :story-id))
+ (:metaclass dao-class)
+ (:keys id))
+
+(deftable task
+ (!dao-def)
+ (!foreign 'story 'story-id 'id))
+
+(defun datainit ()
+ (unless (table-exists-p 'story)
+ (execute (dao-table-definition 'story)))
+ (unless (table-exists-p 'task) (create-table 'task)))
+
+(defun get-all-stories ()
+ (query (:order-by (:select :* (:as (:md5 'assignee) 'md5)
+ :from 'story) 'priority) :alists))
+
+(defun get-stories-for (username)
+ (query (:order-by (:select :* (:as (:md5 'assignee) 'md5)
+ :from 'story
+ :where (:= 'assignee username))
+ 'priority) :alists))
+
+(defun get-story (id)
+ (append (query (:select :* (:as (:md5 'assignee) 'md5) :from 'story
+ :where (:= 'id id)) :alist)
+ `((tasks . ,(get-tasks-for-story id)))))
+
+(defun get-tasks-for-story (id)
+ (query (:order-by (:select :* (:as (:md5 'assignee) 'md5) :from 'task
+ :where (:= 'story-id id))
+ 'priority)
+ :alists))
+
+(defun post-story (role necessity title content reporter)
+ (let ((obj (make-instance
+ 'story :role role :necessity necessity :title title
+ :priority (+ 1 (or (query (:select
+ (:coalesce (:max 'priority) 0)
+ :from 'story) :single)
+ 0))
+ :content content :assignee "" :reporter reporter)))
+ (save-dao obj)))
+
+(defun post-task (story-id description reporter)
+ (let ((obj (make-instance
+ 'task :description description
+ :priority (+ 1 (query (:select
+ (:coalesce (:max 'priority) 0)
+ :from 'task) :single))
+ :reporter reporter :story-id (parse-integer story-id)
+ :assignee "")))
+ (save-dao obj)))
+
+(defun story-get-state (type id)
+ (query (:select 'state :from type :where (:= 'id id)) :single))
+
+(defun story-set-state (type id state)
+ (execute (:update type :set 'state state :where (:= 'id id))))
+
+(defun story-change-priority (type id dir)
+ (let* ((current-priority (query (:select 'priority :from type
+ :where (:= 'id id))
+ :single))
+ (next-priority (funcall (ecase dir (:up #'-) (:down #'+))
+ current-priority 1))
+ (max-priority (query (:select (:max 'priority) :from type)
+ :single)))
+ (execute (:update type :set 'priority current-priority
+ :where (:= 'priority next-priority)))
+ (execute (:update type :set
+ 'priority (max 1 (min next-priority max-priority))
+ :where (:= 'id id)))))
+
+(defun set-assignee (type id assignee)
+ (execute (:update type :set 'assignee assignee
+ :where (:= 'id id))))
diff --git a/defmodule.lisp b/defmodule.lisp
deleted file mode 100644
index aa9335a..0000000
--- a/defmodule.lisp
+++ /dev/null
@@ -1,66 +0,0 @@
-;; scrumli --- A simple scrum web application
-;; Copyright (C) 2013 Tom Willemse
-
-;; scrumli is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU Affero General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; scrumli is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU Affero General Public License for more details.
-
-;; You should have received a copy of the GNU Affero General Public License
-;; along with scrumli. If not, see <http://www.gnu.org/licenses/>.
-
-(restas:define-policy datastore
- (:interface-package #:scrumli.policy.datastore)
- (:interface-method-template "DATASTORE-~A")
- (:internal-package #:scrumli.datastore)
-
- (define-method init ()
- "Initiate the datastore.")
-
- (define-method get-all-stories ()
- "Get all of the stories in the datastore.")
-
- (define-method get-stories-for (username)
- "Get all of the storiess for USERNAME.")
-
- (define-method get-story (id)
- "Get a story from the datastore.")
-
- (define-method get-tasks-for-story (id)
- "Get the tasks associated with a story.")
-
- (define-method post-story (role necessity title content reporter)
- "Post a new story.")
-
- (define-method post-task (story-id description reporter)
- "Post a new task for a story.")
-
- (define-method story-get-state (type id)
- "Get the state of a story.")
-
- (define-method story-set-state (type id state)
- "Set the state of a story.")
-
- (define-method story-change-priority (type id dir)
- "Change the priority of a story in direction DIR.")
-
- (define-method set-assignee (type id assignee)
- "Change the assigned person for a story or task."))
-
-(restas:define-module #:scrumli
- (:use #:cl #:restas #:json #:scrumli.datastore #:drakma)
- (:export #:start-scrumli))
-
-(defpackage #:scrumli.pg-datastore
- (:use #:cl #:postmodern #:scrumli.policy.datastore)
- (:export #:pg-datastore))
-
-(in-package #:scrumli)
-
-(defparameter *static-directory*
- (merge-pathnames #P"static/" scrumli-config:*base-directory*))
diff --git a/packages.lisp b/packages.lisp
new file mode 100644
index 0000000..bee456d
--- /dev/null
+++ b/packages.lisp
@@ -0,0 +1,25 @@
+;; scrumli --- A simple scrum web application
+;; Copyright (C) 2013 Tom Willemse
+
+;; scrumli is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; scrumli is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with scrumli. If not, see <http://www.gnu.org/licenses/>.
+
+(defpackage #:scrumli
+ (:use :cl :ningle :drakma :clack.builder :clack.middleware.session
+ :clack.response :clack.request :clack.middleware.static
+ :postmodern :clack.middleware.postmodern :parenscript)
+ (:import-from :clack :clackup)
+ (:import-from :json :encode-json-to-string :decode-json)
+ (:export #:start-scrumli))
+
+(in-package #:scrumli)
diff --git a/pg-datastore.lisp b/pg-datastore.lisp
deleted file mode 100644
index 5505de1..0000000
--- a/pg-datastore.lisp
+++ /dev/null
@@ -1,136 +0,0 @@
-;; scrumli --- A simple scrum web application
-;; Copyright (C) 2013 Tom Willemse
-
-;; scrumli is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU Affero General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; scrumli is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU Affero General Public License for more details.
-
-;; You should have received a copy of the GNU Affero General Public License
-;; along with scrumli. If not, see <http://www.gnu.org/licenses/>.
-
-(in-package #:scrumli.pg-datastore)
-
-(defclass pg-datastore ()
- ((connection-spec :initarg :connection-spec
- :accessor connection-spec)))
-
-(defclass story ()
- ((id :col-type serial :reader story-id)
- (state :col-type string :reader state :initform "TODO")
- (role :col-type string :reader role :initarg :role)
- (necessity :col-type string :reader necessity :initarg :necessity)
- (title :col-type string :reader title :initarg :title)
- (priority :col-type integer :reader priority :initarg :priority)
- (content :col-type string :reader content :initarg :content)
- (reporter :col-type string :reader reporter :initarg :reporter)
- (assignee :col-type string :reader assignee :initarg :assignee))
- (:metaclass dao-class)
- (:keys id))
-
-(defclass task ()
- ((id :col-type serial :reader story-id)
- (state :col-type string :reader state :initform "TODO")
- (description :col-type string :reader description :initarg :description)
- (priority :col-type integer :reader priority :initarg :priority)
- (reporter :col-type string :reader reporter :initarg :reporter)
- (assignee :col-type string :reader assignee :initarg :assignee)
- (story-id :col-type integer :reader story-id :initarg :story-id))
- (:metaclass dao-class)
- (:keys id))
-
-(deftable task
- (!dao-def)
- (!foreign 'story 'story-id 'id))
-
-(defmethod datastore-init ((datastore pg-datastore))
- (with-connection (connection-spec datastore)
- (unless (table-exists-p 'story)
- (execute (dao-table-definition 'story)))
- (unless (table-exists-p 'task)
- (execute (dao-table-definition 'task)))))
-
-(defmethod datastore-get-all-stories ((datastore pg-datastore))
- (with-connection (connection-spec datastore)
- (query (:order-by (:select :* (:as (:md5 'assignee) 'md5)
- :from 'story) 'priority) :alists)))
-
-(defmethod datastore-get-stories-for ((datastore pg-datastore) username)
- (with-connection (connection-spec datastore)
- (query (:order-by (:select :* (:as (:md5 'assignee) 'md5)
- :from 'story
- :where (:= 'assignee username))
- 'priority) :alists)))
-
-(defmethod datastore-get-story ((datastore pg-datastore) id)
- (with-connection (connection-spec datastore)
- (append (query (:select :* (:as (:md5 'assignee) 'md5) :from 'story
- :where (:= 'id id)) :alist)
- `((tasks . ,(datastore-get-tasks-for-story datastore id))))))
-
-(defmethod datastore-get-tasks-for-story ((datastore pg-datastore) id)
- (with-connection (connection-spec datastore)
- (query (:order-by (:select :* (:as (:md5 'assignee) 'md5) :from 'task
- :where (:= 'story-id id))
- 'priority)
- :alists)))
-
-(defmethod datastore-post-story
- ((datastore pg-datastore) role necessity title content reporter)
- (with-connection (connection-spec datastore)
- (let ((obj (make-instance
- 'story :role role :necessity necessity :title title
- :priority (+ 1 (or (query (:select
- (:coalesce (:max 'priority) 0)
- :from 'story) :single)
- 0))
- :content content :assignee "" :reporter reporter)))
- (save-dao obj))))
-
-(defmethod datastore-post-task
- ((datastore pg-datastore) story-id description reporter)
- (with-connection (connection-spec datastore)
- (let ((obj (make-instance
- 'task :description description
- :priority (+ 1 (query (:select
- (:coalesce (:max 'priority) 0)
- :from 'task) :single))
- :reporter reporter :story-id (parse-integer story-id)
- :assignee "")))
- (save-dao obj))))
-
-(defmethod datastore-story-get-state ((datastore pg-datastore) type id)
- (with-connection (connection-spec datastore)
- (query (:select 'state :from type :where (:= 'id id)) :single)))
-
-(defmethod datastore-story-set-state
- ((datastore pg-datastore) type id state)
- (with-connection (connection-spec datastore)
- (execute (:update type :set 'state state :where (:= 'id id)))))
-
-(defmethod datastore-story-change-priority
- ((datastore pg-datastore) type id dir)
- (with-connection (connection-spec datastore)
- (let* ((current-priority (query (:select 'priority :from type
- :where (:= 'id id))
- :single))
- (next-priority (funcall (ecase dir (:up #'-) (:down #'+))
- current-priority 1))
- (max-priority (query (:select (:max 'priority) :from type)
- :single)))
- (execute (:update type :set 'priority current-priority
- :where (:= 'priority next-priority)))
- (execute (:update type :set
- 'priority (max 1 (min next-priority max-priority))
- :where (:= 'id id))))))
-
-(defmethod datastore-set-assignee
- ((datastore pg-datastore) type id assignee)
- (with-connection (connection-spec datastore)
- (execute (:update type :set 'assignee assignee
- :where (:= 'id id)))))
diff --git a/scrumli.asd b/scrumli.asd
index b3d42c3..a463f94 100644
--- a/scrumli.asd
+++ b/scrumli.asd
@@ -23,11 +23,16 @@
:description "Scrum with Lisp"
:author "Tom Willemse"
:license "AGPLv3"
- :depends-on (:restas :postmodern :cl-json :drakma :closure-template
- :md5)
+ :depends-on (:ningle
+ :postmodern
+ :cl-json
+ :drakma
+ :closure-template
+ :md5
+ :clack-middleware-postmodern
+ :parenscript)
:defsystem-depends-on (:closure-template)
:components ((:closure-template "templates/scrumli")
- (:file "defmodule")
- (:file "pg-datastore")
- (:file "util")
+ (:file "packages")
+ (:file "data")
(:file "scrumli")))
diff --git a/scrumli.lisp b/scrumli.lisp
index 76eff35..bd786a8 100644
--- a/scrumli.lisp
+++ b/scrumli.lisp
@@ -16,7 +16,7 @@
(in-package #:scrumli)
-(defvar *scrumli-host* "http://localhost:8080"
+(defvar *scrumli-host* "http://localhost:5000"
"The host currently running Scrumli. Used by Mozilla Persona.")
(defvar *scrumli-bootstrap-css-location*
@@ -44,7 +44,7 @@
"The location of the JSX Transformer JS file.")
(defun logged-in-p ()
- (hunchentoot:session-value :username))
+ (gethash :username (getf (env *request*) :clack.session)))
(defun page-title (title)
(concatenate 'string title " | scrumli"))
@@ -53,127 +53,6 @@
(string-downcase (format nil "~{~2,'0x~}"
(coerce (md5:md5sum-string str) 'list))))
-(define-route main ("")
- (if (logged-in-p)
- (scrumli-templates:main
- `(:title ,(page-title "Backlog")
- :csss ,(list *scrumli-bootstrap-css-location*
- *scrumli-font-awesome-css-location*
- (genurl 'scrumli-css))
- :jss ,(list *scrumli-jquery-js-location*
- *scrumli-bootstrap-js-location*
- *scrumli-react-js-location*
- *scrumli-jsxtransformer-js-location*)
- :username ,(hunchentoot:session-value :username)
- :usermd5 ,(md5-hash (hunchentoot:session-value :username))
- :ulogout ,(genurl 'logout-page)
- :umainjs ,(genurl 'main-js)))
- (redirect 'login-page)))
-
-(defmacro serve-static (name relpath)
- `(define-route ,name (,relpath :content-type "application/ecmascript")
- (merge-pathnames ,relpath *static-directory*)))
-
-(serve-static main-js "js/main.js")
-(serve-static login-js "js/login.js")
-(serve-static scrumli-css "css/scrumli.css")
-
-(define-route stories-json ("stories" :content-type "text/json")
- (if (logged-in-p)
- (encode-json-to-string (get-all-stories))
- 403))
-
-(define-route my-stories-json ("stories/mine" :content-type "text/json")
- (if (logged-in-p)
- (encode-json-to-string (get-stories-for
- (hunchentoot:session-value :username)))
- 403))
-
-(defmacro with-post-parameters (parameters &body body)
- `(let ,(mapcar (lambda (p)
- (list (intern (string-upcase p))
- `(hunchentoot:post-parameter ,p)))
- parameters)
- ,@body))
-
-(define-route stories-new ("stories/new" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (with-post-parameters ("role" "necessity" "headline" "content")
- (post-story role necessity headline content
- (hunchentoot:session-value :username))
- (encode-json-to-string '((status . "ok"))))
- 403))
-
-(define-route tasks-new ("stories/tasks/new" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (with-post-parameters ("storyId" "description")
- (post-task storyid description
- (hunchentoot:session-value :username))
- (encode-json-to-string '((status . "ok"))))
- 403))
-
-(define-route stories-state ("stories/state" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (let* ((id (hunchentoot:post-parameter "id"))
- (current-state (story-get-state 'story id))
- (next (ecase (intern current-state :scrumli)
- (todo "DOING")
- (doing "DONE")
- (done "TODO"))))
- (story-set-state 'story id next)
- (encode-json-to-string `((status . "ok") (state . ,next))))
- 403))
-
-(define-route task-state ("tasks/state" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (let* ((id (hunchentoot:post-parameter "id"))
- (current-state (story-get-state 'task id))
- (next (ecase (intern current-state :scrumli)
- (todo "DOING")
- (doing "DONE")
- (done "TODO"))))
- (story-set-state 'task id next)
- (encode-json-to-string `((status . "ok") (state . ,next))))
- 403))
-
-(define-route stories-priority ("stories/:dir" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (let* ((id (hunchentoot:post-parameter "id")))
- (story-change-priority
- 'story id (intern (string-upcase dir) :keyword))
- (encode-json-to-string '((status . "ok"))))
- 403))
-
-(define-route task-priority ("tasks/:dir" :method :post
- :content-type "text/json")
- (if (logged-in-p)
- (let* ((id (hunchentoot:post-parameter "id")))
- (story-change-priority
- 'task id (intern (string-upcase dir) :keyword))
- (encode-json-to-string '((status . "ok"))))
- 403))
-
-(define-route login-page ("login")
- (if (not (logged-in-p))
- (scrumli-templates:login
- `(:title ,(page-title "Login")
- :csss ,(list *scrumli-bootstrap-css-location*
- *scrumli-font-awesome-css-location*)
- :jss ,(list *scrumli-bootstrap-js-location*
- "https://login.persona.org/include.js"
- (genurl 'login-js))))
- (redirect 'main)))
-
-(define-route logout-page ("logout")
- (if (logged-in-p)
- (setf (hunchentoot:session-value :username) nil))
- (redirect 'login-page))
-
(defun verify-credentials (audience assertion)
(let ((response
(http-request "https://verifier.login.persona.org/verify"
@@ -184,37 +63,207 @@
:want-stream t)))
(decode-json response)))
-(define-route login-page/post ("login" :method :post)
- (let ((result (verify-credentials
- *scrumli-host*
- (hunchentoot:post-parameter "assertion"))))
- (if (equal (cdr (assoc :status result)) "okay")
- (progn
- (hunchentoot:start-session)
- (setf (hunchentoot:session-value :username)
- (cdr (assoc :email result)))
- (redirect 'main))
- 403)))
-
-(define-route scrumli-story ("stories/:id" :content-type "json")
- (if (logged-in-p)
- (encode-json-to-string (get-story id))
- 403))
-
-(define-route scrumli-story-set-assignee ("story/assignee"
- :content-type "json"
- :method :post)
- (if (logged-in-p)
- (with-post-parameters ("id" "assignee")
- (set-assignee 'story id assignee)
- (encode-json-to-string '((status . "ok"))))
- 403))
-
-(define-route scrumli-task-set-assignee ("tasks/assignee"
- :content-type "json"
- :method :post)
- (if (logged-in-p)
- (with-post-parameters ("id" "assignee")
- (set-assignee 'task id assignee)
- (encode-json-to-string '((status . "ok"))))
- 403))
+(defclass scrumli-app (<app>) ())
+
+(defvar *app* (make-instance 'scrumli-app))
+
+(defun make-tpl-parameters (&rest args)
+ (append (list :prefix (script-name *request*)) args))
+
+(setf (route *app* "/")
+ (lambda (params)
+ (declare (ignore params))
+ (if (logged-in-p)
+ (scrumli-templates:main
+ (make-tpl-parameters
+ :title (page-title "Backlog")
+ :csss (list *scrumli-bootstrap-css-location*
+ *scrumli-font-awesome-css-location*
+ (concatenate 'string (script-name *request*) "static/css/scrumli.css"))
+ :jss (list *scrumli-jquery-js-location*
+ *scrumli-bootstrap-js-location*
+ *scrumli-react-js-location*
+ *scrumli-jsxtransformer-js-location*
+ (concatenate 'string (script-name *request*) "js/bridge.js"))
+ :username (gethash :username (getf (env *request*) :clack.session))
+ :usermd5 (md5-hash (gethash :username (getf (env *request*) :clack.session)))
+ :ulogout (concatenate 'string (script-name *request*) "logout")
+ :umainjs (concatenate 'string (script-name *request*) "static/js/main.js")))
+ (redirect *response*
+ (concatenate 'string (script-name *request*)
+ "login")))))
+
+(setf (route *app* "/login")
+ (lambda (params)
+ (declare (ignore params))
+ (if (not (logged-in-p))
+ (scrumli-templates:login
+ (make-tpl-parameters
+ :title (page-title "Login")
+ :csss (list *scrumli-bootstrap-css-location*
+ *scrumli-font-awesome-css-location*)
+ :jss (list *scrumli-bootstrap-js-location*
+ "https://login.persona.org/include.js"
+ (concatenate 'string (script-name *request*)
+ "js/bridge.js")
+ (concatenate 'string (script-name *request*)
+ "static/js/login.js"))))
+ (redirect *response* (if (equal (script-name *request*) "")
+ "/" (script-name *request*))))))
+
+(setf (route *app* "/login" :method :post)
+ (lambda (params)
+ (let ((result (verify-credentials
+ *scrumli-host* (getf params :|assertion|))))
+ (if (equal (cdr (assoc :status result)) "okay")
+ (progn
+ (setf (gethash :username
+ (getf (env *request*) :clack.session))
+ (cdr (assoc :email result)))
+ (redirect *response*
+ (if (equal (script-name *request*) "")
+ "/" (script-name *request*))))
+ '(403)))))
+
+(setf (route *app* "/logout")
+ (lambda (params)
+ (declare (ignore params))
+ (if (logged-in-p)
+ (setf (gethash :username
+ (getf (env *request*) :clack.session)) nil))
+ (redirect *response* (concatenate 'string (script-name *request*)
+ "login"))))
+
+(setf (route *app* "/stories")
+ (lambda (params)
+ (declare (ignore params))
+ (if (logged-in-p)
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string (get-all-stories)))
+ '(403))))
+
+(setf (route *app* "/stories/mine")
+ (lambda (params)
+ (declare (ignore params))
+ (if (logged-in-p)
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string
+ (get-stories-for
+ (gethash :username (getf (env *request*) :clack.session)))))
+ '(403))))
+
+(setf (route *app* "/stories/new" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let ((role (getf params :|role|))
+ (necessity (getf params :|necessity|))
+ (headline (getf params :|headline|))
+ (content (getf params :|content|)))
+ (post-story role necessity headline content
+ (gethash :username (getf (env *request*) :clack.session)))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/stories/tasks/new" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let ((story-id (getf params :|storyId|))
+ (description (getf params :|description|)))
+ (post-task story-id description
+ (gethash :username (getf (env *request*) :clack.session)))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/stories/state" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let* ((id (getf params :|id|))
+ (current-state (story-get-state 'story id))
+ (next (ecase (intern current-state :scrumli)
+ (todo "DOING")
+ (doing "DONE")
+ (done "TODO"))))
+ (story-set-state 'story id next)
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string `((status . "ok")
+ (state . ,next)))))
+ '(403))))
+
+(setf (route *app* "/tasks/state" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let* ((id (getf params :|id|))
+ (current-state (story-get-state 'task id))
+ (next (ecase (intern current-state :scrumli)
+ (todo "DOING")
+ (doing "DONE")
+ (done "TODO"))))
+ (story-set-state 'task id next)
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string `((status . "ok")
+ (state . ,next)))))
+ '(403))))
+
+(setf (route *app* "/stories/:dir" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let ((id (getf params :|id|))
+ (dir (getf params :dir)))
+ (story-change-priority
+ 'story id (intern (string-upcase dir) :keyword))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/tasks/:dir" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (let ((id (getf params :|id|)))
+ (story-change-priority
+ 'task id (intern (string-upcase (getf params :dir)) :keyword))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/stories/:id")
+ (lambda (params)
+ (if (logged-in-p)
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string (get-story (getf params :id))))
+ '(403))))
+
+(setf (route *app* "/story/assignee" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (progn
+ (set-assignee 'story (getf params :|id|) (getf params :|assignee|))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/task/assignee" :method :post)
+ (lambda (params)
+ (if (logged-in-p)
+ (progn
+ (set-assignee 'task (getf params :|id|) (getf params :|assignee|))
+ (list 200 '(:content-type "text/json")
+ (encode-json-to-string '((status . "ok")))))
+ '(403))))
+
+(setf (route *app* "/js/bridge.js")
+ (lambda (params)
+ (declare (ignore params))
+ (list 200 '(:content-type "text/javascript")
+ (ps (var base-url (lisp (if (equal (script-name *request*) "")
+ "/" (script-name *request*))))))))
+
+(defun get-app ()
+ (builder
+ (<clack-middleware-static> :path "/static/" :root "static/")
+ (<clack-middleware-session>
+ :state (make-instance 'clack.session.state.cookie:<clack-session-state-cookie>))
+ (<clack-middleware-postmodern>
+ :database "scrumli" :user "slash" :password nil :host "localhost")
+ *app*))
diff --git a/start b/start
deleted file mode 100755
index 4a5429b..0000000
--- a/start
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-# scrumli --- A simple scrum web application
-# Copyright (C) 2013 Tom Willemse
-
-# scrumli is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# scrumli is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with scrumli. If not, see <http://www.gnu.org/licenses/>.
-
-sbcl --load scrumli.asd \
- --eval "(asdf:operate 'asdf:load-op \"scrumli\")" \
- --eval "(scrumli:start-scrumli \
- :datastore-init '(:connection-spec \
- (\"scrumli\" \"slash\" nil \"localhost\")))"
diff --git a/static/js/main.js b/static/js/main.js
index 457bd6f..1db43e0 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -43,7 +43,7 @@ var AssigneeIcon = React.createClass({
var StoryTaskRow = React.createClass({
changeState: React.autoBind(function(event) {
- $.post("/tasks/state", {'id': this.props.task.id})
+ $.post(baseUrl + "tasks/state", {'id': this.props.task.id})
.done(function (data, textStatus, jqXHR) {
if (data.status == "ok")
this.setState({state: data.state});
@@ -53,21 +53,21 @@ var StoryTaskRow = React.createClass({
return {state: this.props.task.state};
},
moveUp: React.autoBind(function(event) {
- $.post("/tasks/up", {'id': this.props.task.id})
+ $.post(baseUrl + "tasks/up", {'id': this.props.task.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(1);
}.bind(this));
}),
moveDown: React.autoBind(function(event) {
- $.post("/tasks/down", {'id': this.props.task.id})
+ $.post(baseUrl + "tasks/down", {'id': this.props.task.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(-1);
}.bind(this));
}),
handleAssigneeClick: React.autoBind(function(event) {
- this.props.onAssigneeClicked({url: "/tasks/assignee",
+ this.props.onAssigneeClicked({url: baseUrl + "task/assignee",
id: this.props.task.id,
assignee: this.props.task.assignee,
md5: this.props.task.md5});
@@ -149,14 +149,14 @@ var StoryTaskForm = React.createClass({
var StoryData = React.createClass({
handleTaskSubmit: React.autoBind(function (task) {
task.storyId = this.state.data.id;
- $.post("/stories/tasks/new", task)
+ $.post(baseUrl + "stories/tasks/new", task)
.done(function(data) {
if (data.status == "ok")
this.loadStoryFromServer();
}.bind(this));
}),
loadStoryFromServer: function() {
- $.get("/stories/" + this.state.data.id)
+ $.get(baseUrl + "stories/" + this.state.data.id)
.done(this.setData.bind(this));
},
componentWillMount: function() {
@@ -195,7 +195,7 @@ var StoryData = React.createClass({
var StoryRow = React.createClass({
handleAssigneeClick: React.autoBind(function(event) {
- this.props.onAssigneeClicked({url: "/story/assignee",
+ this.props.onAssigneeClicked({url: baseUrl + "story/assignee",
id: this.props.story.id,
assignee: this.props.story.assignee,
md5: this.props.story.md5});
@@ -240,21 +240,21 @@ var StoryRow = React.createClass({
this.props.onTitleClicked(this.props.story.id);
}),
changeState: React.autoBind(function(event) {
- $.post("/stories/state", {'id': this.props.story.id})
+ $.post(baseUrl + "stories/state", {'id': this.props.story.id})
.done(function(data, textStatus, jqXHR) {
if (data.status == "ok")
this.setState({state: data.state});
}.bind(this));
}),
moveUp: React.autoBind(function(event) {
- $.post("/stories/up", {'id': this.props.story.id})
+ $.post(baseUrl + "stories/up", {'id': this.props.story.id})
.done(function (data, textStatus, jqXHR) {
if (data.status == "ok")
this.props.onMoved(1);
}.bind(this));
}),
moveDown: React.autoBind(function(event) {
- $.post("/stories/down", {'id': this.props.story.id})
+ $.post(baseUrl + "stories/down", {'id': this.props.story.id})
.done(function (data) {
if (data.status == "ok")
this.props.onMoved(-1);
@@ -416,7 +416,7 @@ var StoryPage = React.createClass({
this.loadStoriesFromServer();
}),
handleStorySubmit: React.autoBind(function (story) {
- $.post("/stories/new", story)
+ $.post(baseUrl + "stories/new", story)
.done(function (data, textStatus, jqXHR) {
if (data.status == "ok")
this.loadStoriesFromServer();
@@ -426,7 +426,7 @@ var StoryPage = React.createClass({
}.bind(this));
}),
handleStorySelected: React.autoBind(function (storyId) {
- $.get('/stories/' + storyId)
+ $.get(baseUrl + 'stories/' + storyId)
.done(function (data, textStatus, jqXHR) {
this.refs.data.setData(data);
}.bind(this), 'json');
@@ -466,8 +466,8 @@ var StoryFilter = React.createClass({
handleClick: React.autoBind(function(event) {
this.setState({filter: (this.state.filter != 'all'
? 'all' : 'user')});
- scrumli_page.setUrl((this.state.filter == "all"
- ? "/stories" : "/stories/mine"));
+ scrumli_page.setUrl(baseUrl + (this.state.filter == "all"
+ ? "stories" : "stories/mine"));
}),
render: function() {
var classes = {all: ['icon-group', 'All'],
@@ -480,7 +480,7 @@ var StoryFilter = React.createClass({
}
});
-var scrumli_page = <StoryPage url="/stories" pollInterval={5000} />;
+var scrumli_page = <StoryPage url={baseUrl + "stories"} pollInterval={5000} />;
React.renderComponent(
scrumli_page,
diff --git a/util.lisp b/util.lisp
deleted file mode 100644
index b6fcfdb..0000000
--- a/util.lisp
+++ /dev/null
@@ -1,8 +0,0 @@
-(in-package #:scrumli)
-
-(defun start-scrumli (&key hostname (port 8080)
- (datastore 'scrumli.pg-datastore:pg-datastore)
- datastore-init)
- (setf *datastore* (apply #'make-instance datastore datastore-init))
- (init)
- (start '#:scrumli :port port :hostname hostname))