Initial commit
This commit is contained in:
commit
a993573859
9 changed files with 522 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.emacs.desktop*
|
46
defmodule.lisp
Normal file
46
defmodule.lisp
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
(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-story (id)
|
||||||
|
"Get a story from the datastore.")
|
||||||
|
|
||||||
|
(define-method post-story (role necessity title content reporter)
|
||||||
|
"Post a new story.")
|
||||||
|
|
||||||
|
(define-method story-get-state (id)
|
||||||
|
"Get the state of a story.")
|
||||||
|
|
||||||
|
(define-method story-set-state (id state)
|
||||||
|
"Set the state of a story.")
|
||||||
|
|
||||||
|
(define-method story-change-priority (id dir)
|
||||||
|
"Change the priority of a story in direction DIR."))
|
||||||
|
|
||||||
|
(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*))
|
||||||
|
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
|
(sexml:with-compiletime-active-layers
|
||||||
|
(sexml:standard-sexml sexml:xml-doctype)
|
||||||
|
(sexml:support-dtd
|
||||||
|
(merge-pathnames "html5.dtd" (asdf:system-source-directory "sexml"))
|
||||||
|
:<)))
|
||||||
|
(<:augment-with-doctype "html" "" :auto-emit-p t)
|
65
pg-datastore.lisp
Normal file
65
pg-datastore.lisp
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
(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))
|
||||||
|
|
||||||
|
(defmethod datastore-init ((datastore pg-datastore))
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(unless (table-exists-p 'story)
|
||||||
|
(execute (dao-table-definition 'story)))))
|
||||||
|
|
||||||
|
(defmethod datastore-get-all-stories ((datastore pg-datastore))
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(query (:order-by (:select :* :from 'story) 'priority) :alists)))
|
||||||
|
|
||||||
|
(defmethod datastore-get-story ((datastore pg-datastore) id)
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(query (:select :* :from 'story :where (:= 'id id)) :alist)))
|
||||||
|
|
||||||
|
(defmethod datastore-post-story
|
||||||
|
((datastore pg-datastore) role necessity title content reporter)
|
||||||
|
(format t "~s:~s:~s:~s:~s~%" 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-story-get-state ((datastore pg-datastore) id)
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(query (:select 'state :from 'story :where (:= 'id id)) :single)))
|
||||||
|
|
||||||
|
(defmethod datastore-story-set-state ((datastore pg-datastore) id state)
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(execute (:update 'story :set 'state state :where (:= 'id id)))))
|
||||||
|
|
||||||
|
(defmethod datastore-story-change-priority
|
||||||
|
((datastore pg-datastore) id dir)
|
||||||
|
(with-connection (connection-spec datastore)
|
||||||
|
(let* ((current-priority (query (:select 'priority :from 'story
|
||||||
|
:where (:= 'id id))
|
||||||
|
:single))
|
||||||
|
(next-priority (funcall (ecase dir (:up #'-) (:down #'+))
|
||||||
|
current-priority 1)))
|
||||||
|
(execute (:update 'story :set 'priority current-priority
|
||||||
|
:where (:= 'priority next-priority)))
|
||||||
|
(execute (:update 'story :set 'priority next-priority
|
||||||
|
:where (:= 'id id))))))
|
14
scrumli.asd
Normal file
14
scrumli.asd
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
(defpackage #:scrumli-config (:export #:*base-directory*))
|
||||||
|
(defparameter scrumli-config:*base-directory*
|
||||||
|
(make-pathname :name nil :type nil :defaults *load-truename*))
|
||||||
|
|
||||||
|
(asdf:defsystem #:scrumli
|
||||||
|
:serial t
|
||||||
|
:description "Scrum with Lisp"
|
||||||
|
:author "Tom Willemse"
|
||||||
|
:license "AGPLv3"
|
||||||
|
:depends-on (:restas :sexml :postmodern :cl-json :drakma)
|
||||||
|
:components ((:file "defmodule")
|
||||||
|
(:file "pg-datastore")
|
||||||
|
(:file "util")
|
||||||
|
(:file "scrumli")))
|
153
scrumli.lisp
Normal file
153
scrumli.lisp
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
(in-package #:scrumli)
|
||||||
|
|
||||||
|
(defvar *scrumli-host* "http://localhost:8080"
|
||||||
|
"The host currently running Scrumli. Used by Mozilla Persona.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-bootstrap-css-location*
|
||||||
|
"http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css"
|
||||||
|
"The location of the twitter bootstrap CSS file.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-bootstrap-js-location*
|
||||||
|
"http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"
|
||||||
|
"The location of the twitter bootstrap JS file.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-font-awesome-css-location*
|
||||||
|
"http://netdna.bootstrapcdn.com/font-awesome/3.1.1/css/font-awesome.min.css"
|
||||||
|
"The location of the font awesome CSS file.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-jquery-js-location*
|
||||||
|
"http://code.jquery.com/jquery-2.0.0.min.js"
|
||||||
|
"The location of the jQuery JS file.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-react-js-location*
|
||||||
|
"http://cdnjs.cloudflare.com/ajax/libs/react/0.3.2/react.min.js"
|
||||||
|
"The location of the React JS file.")
|
||||||
|
|
||||||
|
(defvar *scrumelo-jsxtransformer-js-location*
|
||||||
|
"http://cdnjs.cloudflare.com/ajax/libs/react/0.3.2/JSXTransformer.js"
|
||||||
|
"The location of the JSX Transformer JS file.")
|
||||||
|
|
||||||
|
(defun logged-in-p ()
|
||||||
|
(hunchentoot:session-value :username))
|
||||||
|
|
||||||
|
(define-route main ("")
|
||||||
|
(if (logged-in-p)
|
||||||
|
(<:html
|
||||||
|
(<:head (<:title "Hello!")
|
||||||
|
(<:link :href *scrumelo-bootstrap-css-location*
|
||||||
|
:rel "stylesheet" :type "text/css")
|
||||||
|
(<:link :href *scrumelo-font-awesome-css-location*
|
||||||
|
:rel "stylesheet" :type "text/css")
|
||||||
|
(<:script :type "text/javascript"
|
||||||
|
:src *scrumelo-bootstrap-js-location*)
|
||||||
|
(<:script :type "text/javascript"
|
||||||
|
:src *scrumelo-jquery-js-location*)
|
||||||
|
(<:script :type "text/javascript"
|
||||||
|
:src *scrumelo-react-js-location*)
|
||||||
|
(<:script :type "text/javascript"
|
||||||
|
:src *scrumelo-jsxtransformer-js-location*))
|
||||||
|
(<:body
|
||||||
|
(<:a :href "/logout" "Logout") " "
|
||||||
|
(hunchentoot:session-value :username)
|
||||||
|
(<:div :class "container"
|
||||||
|
(<:h1 "Backlog")
|
||||||
|
(<:div :id "content")
|
||||||
|
(<:script :type "text/jsx" :src "js/main.js"))))
|
||||||
|
(redirect 'login-page)))
|
||||||
|
|
||||||
|
(define-route react-ui ("js/main.js"
|
||||||
|
:content-type "application/ecmascript")
|
||||||
|
(merge-pathnames "js/main.js" *static-directory*))
|
||||||
|
|
||||||
|
(define-route login-js ("js/login.js"
|
||||||
|
:content-type "application/ecmascript")
|
||||||
|
(merge-pathnames "js/login.js" *static-directory*))
|
||||||
|
|
||||||
|
(define-route stories-json ("stories" :content-type "text/json")
|
||||||
|
(if (logged-in-p)
|
||||||
|
(with-output-to-string (out)
|
||||||
|
(encode-json (get-all-stories) out))
|
||||||
|
403))
|
||||||
|
|
||||||
|
(define-route stories-new ("stories/new" :method :post)
|
||||||
|
(if (logged-in-p)
|
||||||
|
(let ((role (hunchentoot:post-parameter "role"))
|
||||||
|
(necessity (hunchentoot:post-parameter "necessity"))
|
||||||
|
(title (hunchentoot:post-parameter "headline"))
|
||||||
|
(content (hunchentoot:post-parameter "content")))
|
||||||
|
(format t "~s;~s;~s;~s;~s~%" role necessity title content
|
||||||
|
(hunchentoot:session-value :username))
|
||||||
|
(post-story role necessity title content
|
||||||
|
(hunchentoot:session-value :username))
|
||||||
|
200)
|
||||||
|
403))
|
||||||
|
|
||||||
|
(define-route stories-state ("stories/state" :method :post)
|
||||||
|
(if (logged-in-p)
|
||||||
|
(let* ((id (hunchentoot:post-parameter "id"))
|
||||||
|
(current-state (story-get-state id)))
|
||||||
|
(story-set-state id (ecase (intern current-state :scrumli)
|
||||||
|
(todo "DOING")
|
||||||
|
(doing "DONE")
|
||||||
|
(done "TODO")))
|
||||||
|
200)
|
||||||
|
403))
|
||||||
|
|
||||||
|
(define-route stories-priority ("stories/:dir" :method :post)
|
||||||
|
(if (logged-in-p)
|
||||||
|
(let* ((id (hunchentoot:post-parameter "id")))
|
||||||
|
(story-change-priority id (intern (string-upcase dir) :keyword))
|
||||||
|
200)
|
||||||
|
403))
|
||||||
|
|
||||||
|
(define-route login-page ("login")
|
||||||
|
(if (not (logged-in-p))
|
||||||
|
(<:html :lang "en"
|
||||||
|
(<:head (<:meta :charset "utf-8")
|
||||||
|
(<:title "Login")
|
||||||
|
(<:script :src "https://login.persona.org/include.js")
|
||||||
|
(<:script :src "/js/login.js"))
|
||||||
|
(<:body
|
||||||
|
(<:form :id "login-form" :method "POST" :action ""
|
||||||
|
(<:input :id "assertion-field" :type "hidden"
|
||||||
|
:name "assertion" :value ""))
|
||||||
|
(<:p (<:a :href "javascript:login()" "Login"))))
|
||||||
|
(redirect 'main)))
|
||||||
|
|
||||||
|
(define-route logout-page ("logout")
|
||||||
|
(if (logged-in-p)
|
||||||
|
(progn
|
||||||
|
(setf (hunchentoot:session-value :username) nil)))
|
||||||
|
(redirect 'login-page))
|
||||||
|
|
||||||
|
(defun json-to-string (obj)
|
||||||
|
(with-output-to-string (out)
|
||||||
|
(encode-json obj out)))
|
||||||
|
|
||||||
|
(defun verify-credentials (audience assertion)
|
||||||
|
(let ((response
|
||||||
|
(http-request "https://verifier.login.persona.org/verify"
|
||||||
|
:method :post :content-type "application/json"
|
||||||
|
:content (json-to-string
|
||||||
|
`(("assertion" . ,assertion)
|
||||||
|
("audience" . ,audience)))
|
||||||
|
: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 scrumelo-story ("stories/:id")
|
||||||
|
(if (logged-in-p)
|
||||||
|
(with-output-to-string (out)
|
||||||
|
(encode-json (get-story id) out))
|
||||||
|
403))
|
7
start
Executable file
7
start
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sbcl --load scrumli.asd \
|
||||||
|
--eval "(asdf:operate 'asdf:load-op \"scrumli\")" \
|
||||||
|
--eval "(scrumli:start-scrumli \
|
||||||
|
:datastore-init '(:connection-spec \
|
||||||
|
(\"scrumli\" \"slash\" nil \"localhost\")))"
|
12
static/js/login.js
Normal file
12
static/js/login.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
function login ()
|
||||||
|
{
|
||||||
|
navigator.id.get(function(assertion) {
|
||||||
|
if (assertion) {
|
||||||
|
var assertion_field =
|
||||||
|
document.getElementById("assertion-field");
|
||||||
|
assertion_field.value = assertion;
|
||||||
|
var login_form = document.getElementById("login-form");
|
||||||
|
login_form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
215
static/js/main.js
Normal file
215
static/js/main.js
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/** @jsx React.DOM */
|
||||||
|
var StateIcon = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
var icon_names = {"TODO": "icon-check-empty",
|
||||||
|
"DOING": "icon-sign-blank",
|
||||||
|
"DONE": "icon-check"};
|
||||||
|
return <i class={icon_names[this.props.state]}></i>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryData = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
if (this.props.data) {
|
||||||
|
return (<div>
|
||||||
|
Assignee: {this.props.data.assignee}
|
||||||
|
<pre>{this.props.data.content}</pre>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryRow = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
// A little ugly to get a space, but I don't know of any other
|
||||||
|
// way.
|
||||||
|
var state = " " + this.state.state;
|
||||||
|
var sdata = null;
|
||||||
|
|
||||||
|
if (this.state.content)
|
||||||
|
sdata = <StoryData data={this.state.content} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="icon-arrow-up" onClick={this.moveUp}></i>
|
||||||
|
<i class="icon-arrow-down" onClick={this.moveDown}></i>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span onClick={this.changeState}>
|
||||||
|
<StateIcon state={this.state.state} />
|
||||||
|
{state}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a onClick={this.handleClick}>
|
||||||
|
As a {this.props.story.role}, I
|
||||||
|
{this.props.story.necessity} to
|
||||||
|
{this.props.story.title}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
{sdata}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {state: this.props.story.state,
|
||||||
|
content: null};
|
||||||
|
},
|
||||||
|
handleClick: React.autoBind(function(event) {
|
||||||
|
if (!!this.state.content) {
|
||||||
|
this.setState({content: null});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$.get('/stories/' + this.props.story.id, null,
|
||||||
|
function (data, textStatus, jqXHR) {
|
||||||
|
self.setState({content: data});
|
||||||
|
}, 'json');
|
||||||
|
}),
|
||||||
|
changeState: React.autoBind(function(event) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stories/state",
|
||||||
|
type: "POST",
|
||||||
|
data: {'id': this.props.story.id},
|
||||||
|
dataType: 'json',
|
||||||
|
mimeType: 'textPlain',
|
||||||
|
success: function(data) {
|
||||||
|
this.setState({state: eval(data).state});
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
moveUp: React.autoBind(function(event) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stories/up",
|
||||||
|
type: "POST",
|
||||||
|
data: {'id': this.props.story.id},
|
||||||
|
dataType: 'json',
|
||||||
|
mimeType: 'textPlain',
|
||||||
|
success: function (data) {
|
||||||
|
this.props.onMoved(1);
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
moveDown: React.autoBind(function(event) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stories/down",
|
||||||
|
type: "POST",
|
||||||
|
data: {'id': this.props.story.id},
|
||||||
|
dataType: 'json',
|
||||||
|
mimeType: 'textPlain',
|
||||||
|
success: function (data) {
|
||||||
|
this.props.onMoved(-1);
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryTable = React.createClass({
|
||||||
|
loadStoriesFromServer: function() {
|
||||||
|
$.ajax({
|
||||||
|
url: this.props.url,
|
||||||
|
mimeType: 'textPlain',
|
||||||
|
success: function(data) {
|
||||||
|
this.setState({data: eval(data)});
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {data: []};
|
||||||
|
},
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.loadStoriesFromServer();
|
||||||
|
setInterval(
|
||||||
|
this.loadStoriesFromServer.bind(this),
|
||||||
|
this.props.pollInterval
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleMoved: React.autoBind(function(direction) {
|
||||||
|
this.loadStoriesFromServer();
|
||||||
|
}),
|
||||||
|
render: function() {
|
||||||
|
var storyNodes = this.state.data.map(function (story) {
|
||||||
|
return <StoryRow story={story} onMoved={this.handleMoved} />;
|
||||||
|
}.bind(this));
|
||||||
|
return (
|
||||||
|
<table class="table table-striped">
|
||||||
|
{storyNodes}
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryForm = React.createClass({
|
||||||
|
handleSubmit: React.autoBind(function() {
|
||||||
|
var role = this.refs.role.getDOMNode().value.trim();
|
||||||
|
var necessity = this.refs.necessity.getDOMNode().value.trim();
|
||||||
|
var headline = this.refs.headline.getDOMNode().value.trim();
|
||||||
|
var content = this.refs.content.getDOMNode().value.trim();
|
||||||
|
|
||||||
|
this.props.onStorySubmit({role: role,
|
||||||
|
necessity: necessity,
|
||||||
|
headline: headline,
|
||||||
|
content: content});
|
||||||
|
|
||||||
|
this.refs.role.getDOMNode().value = '';
|
||||||
|
this.refs.necessity.getDOMNode().value = '';
|
||||||
|
this.refs.headline.getDOMNode().value = '';
|
||||||
|
this.refs.content.getDOMNode().value = '';
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="toggle">New story</legend>
|
||||||
|
<div id="new-story">
|
||||||
|
<div class="input-prepend input-append">
|
||||||
|
<span class="add-on">As a </span>
|
||||||
|
<input type="text" class="input-medium" ref="role"
|
||||||
|
placeholder="person" />
|
||||||
|
<span class="add-on"> I </span>
|
||||||
|
<input type="text" class="input-small"
|
||||||
|
ref="necessity" placeholder="would like" />
|
||||||
|
<span class="add-on"> to </span>
|
||||||
|
<input type="text" class="input-xxlarge"
|
||||||
|
ref="headline" placeholder="fill in this form..." />
|
||||||
|
<button class="btn" type="submit">!</button><br />
|
||||||
|
<textarea ref="content"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StoryPage = React.createClass({
|
||||||
|
handleStorySubmit: React.autoBind(function (story) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stories/new",
|
||||||
|
type: "POST",
|
||||||
|
data: story,
|
||||||
|
dataType: 'json',
|
||||||
|
mimeType: 'textPlain'
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StoryTable url="/stories" pollInterval={5000} />
|
||||||
|
<StoryForm onStorySubmit={this.handleStorySubmit} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
React.renderComponent(
|
||||||
|
<StoryPage />,
|
||||||
|
document.getElementById('content')
|
||||||
|
);
|
9
util.lisp
Normal file
9
util.lisp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
(in-package #:scrumli)
|
||||||
|
|
||||||
|
(defun start-scrumli (&key
|
||||||
|
(port 8080)
|
||||||
|
(datastore 'scrumli.pg-datastore:pg-datastore)
|
||||||
|
datastore-init)
|
||||||
|
(setf *datastore* (apply #'make-instance datastore datastore-init))
|
||||||
|
(init)
|
||||||
|
(start '#:scrumli :port port))
|
Loading…
Reference in a new issue