From 781befeb23bdbe90f1a8220da86d39ab6fd012fb Mon Sep 17 00:00:00 2001 From: Tom Willemse Date: Mon, 29 Jul 2013 02:09:34 +0200 Subject: [PATCH] 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. --- data.lisp | 115 +++++++++++++++ defmodule.lisp | 66 --------- packages.lisp | 25 ++++ pg-datastore.lisp | 136 ------------------ scrumli.asd | 15 +- scrumli.lisp | 357 ++++++++++++++++++++++++++-------------------- start | 22 --- static/js/main.js | 30 ++-- util.lisp | 8 -- 9 files changed, 368 insertions(+), 406 deletions(-) create mode 100644 data.lisp delete mode 100644 defmodule.lisp create mode 100644 packages.lisp delete mode 100644 pg-datastore.lisp delete mode 100755 start delete mode 100644 util.lisp 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 . + +(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 . - -(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 . + +(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 . - -(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))) +(defclass scrumli-app () ()) -(define-route scrumli-story ("stories/:id" :content-type "json") - (if (logged-in-p) - (encode-json-to-string (get-story id)) - 403)) +(defvar *app* (make-instance 'scrumli-app)) -(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)) +(defun make-tpl-parameters (&rest args) + (append (list :prefix (script-name *request*)) args)) -(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)) +(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 + ( :path "/static/" :root "static/") + ( + :state (make-instance 'clack.session.state.cookie:)) + ( + :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 . - -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 = ; +var scrumli_page = ; 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))