The goal of feature flow is to provide value as quickly as
7 | possible. Examples of value include enabling a customer to do something
8 | new, or more easily, increasing efficiency, or learning something
9 | about a customer or market
10 |
11 |
Boards
12 |
A board displays the features a team is
13 | working on. You can navigate to a board by clicking on it's
14 | name in the board menu, which you can view by clicking on
15 | the menu icon in the upper-left corner of the window. (You
16 | can get back here by clicking on the home icon in the
17 | menu.)
18 |
If you're an administrator, you can add a
19 | board from this menu.
20 |
21 |
Features
22 |
Features are the focus of team work.
23 |
At a high level, a board shows features, in
24 | the backlog, or in one of the feature states
25 |
26 |
Tasks
27 |
Features are implemented by one or more tasks.
28 |
29 |
30 |
31 |
Using
32 |
Drag feature and task cards between states.
33 |
Click on plus buttons to add features or tasks.
34 |
Click on pencil buttons to edit features.
35 |
Click on tasks to edit them.
36 |
37 |
38 |
--------------------------------------------------------------------------------
/client/demo/siteapi.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid/v1';
2 |
3 | import {Site} from '../model/site';
4 |
5 | import {BaseAPI} from './baseapi';
6 |
7 | export class SiteAPI extends BaseAPI {
8 |
9 | constructor(view, cb) {
10 | super(new Site(), view, cb);
11 | }
12 |
13 | poll(cb) {
14 | super.poll().then((db) => {
15 | this.transaction(['boards', 'users'], 'readonly', (trans) => {
16 | this.boards(trans, (boards) => {
17 | this.users(trans, (users, user) => {
18 | this.update(
19 | trans,
20 | {site: {boards: boards, users: users}, user: user},
21 | cb);
22 | });
23 | });
24 | });
25 | });
26 | }
27 |
28 | add_user(email, name, admin, cb) {
29 | this.transaction('users', 'readwrite', (trans) => {
30 | const users_store = trans.objectStore('users');
31 | this.r(users_store.add(
32 | {id: uuid(), email: email, name: name, admin: admin, nick:''}), () => {
33 | this.all(users_store.openCursor(), (users) => {
34 | this.update(trans, {site: {users: users}}, cb);
35 | }, cb);
36 | }, cb);
37 | }, cb);
38 | }
39 |
40 | change_user_type(uid, admin, cb) {
41 | this.transaction('users', 'readwrite', (trans) => {
42 | const users_store = trans.objectStore('users');
43 | this.r(users_store.get(uid), (user) => {
44 | user.admin = admin;
45 | this.r(users_store.put(user), () => {
46 | this.all(users_store.openCursor(), (users) => {
47 | this.update(trans, {site: {users: users}}, cb);
48 | }, cb);
49 | }, cb);
50 | }, cb);
51 | }, cb);
52 | }
53 |
54 | get_requests(f) {
55 | setTimeout(() => f([]), 10);
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/server/twotieredkanban/tests/testtask.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 | import unittest
3 |
4 | class TaskTests(unittest.TestCase):
5 |
6 | @mock.patch('twotieredkanban.board.now')
7 | def test_creation(self, now):
8 | from ..board import State, Task
9 |
10 | now.return_value = '2017-06-08T10:02:00.004'
11 | import uuid
12 | id = uuid.uuid1()
13 | with mock.patch('uuid.uuid1', return_value=id):
14 | task = Task(self, 'the task', 42, 'the description',
15 | state = State(1, 'test'))
16 |
17 | self.assertEqual(self, task.board)
18 |
19 | self.assertEqual(
20 | dict(id = id.hex,
21 | title = 'the task',
22 | description = 'the description',
23 | order = 42,
24 | state = 'test',
25 | parent = None,
26 | blocked = None,
27 | assigned = None,
28 | size = 1,
29 | history=(
30 | dict(start='2017-06-08T10:02:00.004', state='test'),
31 | ),
32 | ),
33 | task.json_reduce())
34 |
35 | task.tasks = []
36 | self.assertEqual(
37 | dict(id = id.hex,
38 | title = 'the task',
39 | description = 'the description',
40 | order = 42,
41 | state = 'test',
42 | parent = None,
43 | blocked = None,
44 | assigned = None,
45 | size = 1,
46 | history=(
47 | dict(start='2017-06-08T10:02:00.004', state='test'),
48 | ),
49 | tasks = [],
50 | ),
51 | task.json_reduce())
52 |
--------------------------------------------------------------------------------
/client/model/boardapi.js:
--------------------------------------------------------------------------------
1 | import {APIBase} from './apibase';
2 | import {Board} from './board';
3 |
4 | export class BoardAPI extends APIBase {
5 |
6 | constructor(view, name) {
7 | super(new Board(name), view, '/board/' + name + '/');
8 | }
9 |
10 | rename(name) {
11 | this.put('', {name: name});
12 | }
13 |
14 | add_project(props) {
15 | this.post('projects',
16 | {title: props.title, description: props.description,
17 | order: this.model.order(undefined, true)});
18 | }
19 |
20 | add_task(props) {
21 | this.post('project/' + props.project_id, {
22 | title: props.title,
23 | description: props.description,
24 | size: props.size,
25 | blocked: props.blocked,
26 | assigned: props.assigned,
27 | order: this.model.order(undefined, true)
28 | });
29 | }
30 |
31 | update_task(id, props) {
32 | this.put('tasks/' + id, {
33 | title: props.title,
34 | description: props.description,
35 | size: props.size,
36 | blocked: props.blocked,
37 | assigned: props.assigned
38 | });
39 | }
40 |
41 | remove(id) {
42 | this.delete('tasks/' + id);
43 | }
44 |
45 | move(task_id, parent_id, state_id, before_id, front) {
46 | const order = this.model.order(before_id, front);
47 | this.put(`move/${task_id}`,
48 | {state_id: state_id, parent_id: parent_id, order: order});
49 | }
50 |
51 | archive(feature_id) {
52 | this.post('archive/' + feature_id, {});
53 | }
54 |
55 | restore(feature_id) {
56 | this.delete('archive/' + feature_id);
57 | }
58 |
59 | get_archived(search, start, size, f) {
60 | search = search ? '&text=' + encodeURIComponent(search) : '';
61 | this.get('archive?start=' + start + '&size=' + size + search).then((r) => {
62 | r.data.start = start;
63 | this.model.update({search: {archive: r.data}});
64 | this.view.setState({model: this.model});
65 | });
66 | }
67 |
68 | export_url(f) {
69 | f(this.base + 'export');
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/BUILDING.rst:
--------------------------------------------------------------------------------
1 | Building the app
2 | ================
3 |
4 | There are currently 2 modes:
5 |
6 | demo
7 | Data are stored in the browser and can't be shared.
8 |
9 | server
10 | Data are stored on the server.
11 |
12 | Note that we're still punting on authentication and user management.
13 |
14 | Demo mode
15 | =========
16 |
17 | Build::
18 |
19 | npm install
20 | webpack --env.demo
21 |
22 | Run
23 | You have 2 options.
24 |
25 | If you're using Chrome:
26 | Open ``demo/index.html`` in your browser. If the browser
27 | generates a ``file::`` url by following the symbolic link, edit the URL
28 | to end in ``demo/index.html``.
29 |
30 | If you're using Firefox or Safari
31 | The browsers don't seem to work correctly with ``file://`` urls,
32 | so you'll want to host the demo files with a web server. You can
33 | use the express server. See express/README.rst
34 |
35 | We're mostly developing with Chrome. There are a number of
36 | problems using other browsers at this point.
37 |
38 | Server mode
39 | ===========
40 |
41 | To build, run the buildout. This will build the Python app, run npm,
42 | and webpack.
43 |
44 | Create a local ``kanban`` Postgres database. Alternatively, supply an
45 | alternate connection string when you run buildout::
46 |
47 | bin/buildout database=postgresql://myuser@myhost/dbname
48 |
49 | To run::
50 |
51 | bin/app fg
52 |
53 | (Use ``start`` rather than ``fg`` to run the server in the background.)
54 |
55 | Before accessing the Kanban for the first time, you will need to
56 | invite a (bootstrap) user::
57 |
58 | bin/emailpw-bootstrap -bd -t "mysite" db.cfg localhost jim@jimfulton.info 'Jim Fulton'
59 |
60 | This will print an "email" message with a URL, which will look
61 | something like::
62 |
63 | http://localhost:8000/auth/accept?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImppbUBqaW1mdWx0b24uaW5mbyJ9.iZRzDFb5-yKFQB0xJv1Pg5uicQG4hImOJiAe8ncJ9_o
64 |
65 | Opem the URL in your browser. That should present a page to set your
66 | password and then log in.
67 |
68 |
--------------------------------------------------------------------------------
/server/twotieredkanban/sql/__init__.py:
--------------------------------------------------------------------------------
1 | from contextlib import closing
2 | import os
3 | import re
4 |
5 | from newt.db import pg_connection
6 | import newt.qbe
7 |
8 | here = os.path.dirname(__file__)
9 | evolver = re.compile(r'evolve(\d+)$').match
10 |
11 | def evolve(dsn):
12 | with closing(pg_connection(dsn)) as conn:
13 | with closing(conn.cursor()) as cursor:
14 | try:
15 | with conn:
16 | cursor.execute("select version from ff_schema_version")
17 | [[version]] = cursor
18 | except Exception:
19 | with conn:
20 | cursor.execute("""
21 | create table ff_schema_version(version int);
22 | insert into ff_schema_version values(0)
23 | """)
24 | version = 0
25 |
26 | for v, name, ob in sorted((int(evolver(name).group(1)), name, ob)
27 | for (name, ob) in globals().items()
28 | if evolver(name)):
29 | if v > version:
30 | print(name)
31 | with conn:
32 | if isinstance(ob, str):
33 | if ' ' not in ob:
34 | with open(os.path.join(here, ob)) as f:
35 | ob = f.read()
36 | cursor.execute(ob)
37 | else:
38 | ob(conn, cursor)
39 | cursor.execute(
40 | "update ff_schema_version set version = %s",
41 | (v,))
42 |
43 | evolve1 = 'evolve1.sql'
44 |
45 |
46 | qbe = newt.qbe.QBE()
47 |
48 | qbe['text'] = newt.qbe.fulltext("extract_text(class_name, state)", "english")
49 | qbe['archived'] = newt.qbe.scalar("state -> 'history' -> -1 ->> 'archived'")
50 | qbe['modified'] = newt.qbe.scalar("state -> 'history' -> -1 ->> 'start'")
51 | qbe['board'] = newt.qbe.scalar("(state -> 'board' ->> '::=>')::bigint")
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "repository": {
4 | "type": "git",
5 | "url": "https://github.com/feature-flow/twotieredkanban.git"
6 | },
7 | "version": "1.0.0",
8 | "description": "simple react app",
9 | "main": "index.js",
10 | "scripts": {
11 | "test": "mocha-webpack --webpack-config webpack.config-test.js \"client/tests/test*.js\" --require source-map-support/register"
12 | },
13 | "author": "Jim Fulton",
14 | "license": "MIT",
15 | "dependencies": {
16 | "axios": "^0.19.0",
17 | "classnames": "^2.2.5",
18 | "draft-js": "^0.10.1",
19 | "material-design-icons": "^3.0.1",
20 | "md5": "^2.2.1",
21 | "raven-js": "^3.15.0",
22 | "react": "~15.5.0",
23 | "react-addons-css-transition-group": "~15.5.0",
24 | "react-dom": "~15.5.0",
25 | "react-router": "^2.0.0",
26 | "react-rte": "^0.11.0",
27 | "react-toolbox": "^2.0.0-beta.12",
28 | "uuid": "^3.0.1"
29 | },
30 | "devDependencies": {
31 | "babel": "^6.23.0",
32 | "babel-core": "^6.24.1",
33 | "babel-loader": "^7.0.0",
34 | "babel-preset-es2015": "^6.24.1",
35 | "babel-preset-react": "^6.24.1",
36 | "babel-register": "^6.24.1",
37 | "camelcase": "^4.1.0",
38 | "chai": "^3.5.0",
39 | "css-loader": "^0.28.1",
40 | "expect": "^1.20.2",
41 | "fake-indexeddb": "^2.0.3",
42 | "html-loader": "^0.4.5",
43 | "jquery": "^3.2.1",
44 | "json-loader": "^0.5.4",
45 | "karma": "^1.7.0",
46 | "karma-chrome-launcher": "^2.1.1",
47 | "karma-mocha": "^1.3.0",
48 | "karma-mocha-reporter": "^2.2.3",
49 | "karma-sourcemap-loader": "^0.3.7",
50 | "karma-webpack": "^2.0.3",
51 | "mocha": "^3.3.0",
52 | "mocha-webpack": "^0.7.0",
53 | "mockdate": "^2.0.1",
54 | "node-sass": "^4.5.3",
55 | "postcss-cssnext": "^2.10.0",
56 | "postcss-each": "^0.9.3",
57 | "postcss-import": "^9.1.0",
58 | "postcss-loader": "^1.3.3",
59 | "postcss-mixins": "^5.4.1",
60 | "react-addons-test-utils": "~15.5.0",
61 | "sass-loader": "^6.0.3",
62 | "script-loader": "^0.7.0",
63 | "source-map-support": "^0.4.15",
64 | "style-loader": "^0.17.0",
65 | "webpack": "^2.5.1",
66 | "webpack-node-externals": "^1.6.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/client/demo/intro.html:
--------------------------------------------------------------------------------
1 |
2 |
Welcome to the Valuenator Demo
3 |
4 |
In this demo version of Valuenator, you can work with tasks just as you
5 | would in the on-line version, except that:
6 |
7 |
All data is stored locally and persistently in your browser.
8 | (If you reload the demo, the data will still be there.)
9 |
Therefore, you can't share tasks with other people.
10 |
Searching in the demo version uses substring search, as opposed
11 | to the on-line version which uses full-text (language aware)
12 | search.
13 |
14 |
15 |
Concepts
16 |
The goal of feature flow is to provide value as quickly as
17 | possible. Examples of value include enabling a customer to do something
18 | new, or more easily, increasing efficiency, or learning something
19 | about a customer or market
20 |
21 |
Boards
22 |
A board displays the features a team is
23 | working on. You can navigate to a board by clicking on it's
24 | name in the board menu, which you can view by clicking on
25 | the menu icon in the upper-left corner of the window. (You
26 | can get back here by clicking on the home icon in the
27 | menu.)
28 |
If you're an administrator, you can add a
29 | board from this menu.
30 |
31 |
Features
32 |
Features are the focus of team work.
33 |
At a high level, a board shows features, in
34 | the backlog, or in one of the feature states
35 |
36 |
Tasks
37 |
Features are implemented by one or more tasks.
38 |
39 |
40 |
41 |
Using
42 |
Drag feature and task cards between states.
43 |
Click on plus buttons to add features or tasks.
44 |
Click on pencil buttons to edit features.
45 |
Click on tasks to edit them.
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Fri May 15 2015 10:50:23 GMT-0400 (EDT)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | "static/angular/angular.js",
19 | "node_modules/angular-mocks/angular-mocks.js",
20 | "static/angular-animate/angular-animate.js",
21 | "static/angular-aria/angular-aria.js",
22 | "static/angular-material/angular-material.js",
23 | "static/angular-material-icons/angular-material-icons.js",
24 | "static/angular-sanitize/angular-sanitize.js",
25 | 'static/*.js'
26 | ],
27 |
28 |
29 | // list of files to exclude
30 | exclude: [
31 | ],
32 |
33 |
34 | // preprocess matching files before serving them to the browser
35 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
36 | preprocessors: {
37 | 'static/*.js': ['sourcemap']
38 | },
39 |
40 |
41 | // test results reporter to use
42 | // possible values: 'dots', 'progress'
43 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
44 | reporters: ['progress'],
45 |
46 |
47 | // web server port
48 | port: 9876,
49 |
50 |
51 | // enable / disable colors in the output (reporters and logs)
52 | colors: false,
53 |
54 |
55 | // level of logging
56 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
57 | logLevel: config.LOG_INFO,
58 |
59 |
60 | // enable / disable watching file and executing tests whenever any file changes
61 | autoWatch: true,
62 |
63 |
64 | // start these browsers
65 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
66 | browsers: ['Chrome'],
67 |
68 |
69 | // Continuous Integration mode
70 | // if true, Karma captures browsers, runs the tests and exits
71 | singleRun: false
72 | });
73 | };
74 |
--------------------------------------------------------------------------------
/server/twotieredkanban/apiutil.py:
--------------------------------------------------------------------------------
1 | import bobo
2 | import datetime
3 | import json
4 | import webob
5 |
6 | def check(self, request, func):
7 | return self.check(func)
8 |
9 | def get(*args, **kw):
10 | return bobo.get(*args, check=check, **kw)
11 |
12 | def post(*args, **kw):
13 | return bobo.post(*args, check=check, **kw)
14 |
15 | def put(*args, **kw):
16 | return bobo.put(*args, check=check, **kw)
17 |
18 | def delete(*args, **kw):
19 | return bobo.delete(*args, check=check, **kw)
20 |
21 | class Encoder(json.JSONEncoder):
22 |
23 | def default(self, obj):
24 | if isinstance(obj, datetime.datetime):
25 | return obj.isoformat()[:19]
26 | return obj.json_reduce()
27 |
28 | class Sync:
29 |
30 | def __init__(self, base, context):
31 | self.base = base
32 | self.context = context
33 |
34 | def check(self, func=None):
35 | return self.base.check(func)
36 |
37 | def _response(self, data=None):
38 | response = webob.Response(content_type="application/json")
39 | response.text = json.dumps(data, cls=Encoder) if data else '{}'
40 | response.cache_control = 'no-cache'
41 | response.pragma = 'no-cache'
42 | return response
43 |
44 | def response(self, send_user=None, **data):
45 | generation = int(self.base.request.headers.get('x-generation', 0))
46 | updates = self.context.updates(generation)
47 | if generation == 0:
48 | # first load, set uswer
49 | updates['user'] = self.base.user
50 | if raven:
51 | updates['raven'] = dict(
52 | url=raven,
53 | options=dict(tags=dict(server_release=release))
54 | )
55 |
56 | if send_user:
57 | updates['user'] = send_user
58 |
59 | if updates:
60 | data['updates'] = updates
61 | return self._response(data)
62 |
63 | @get("/longpoll")
64 | @get("/poll")
65 | def poll(self):
66 | return self.response()
67 |
68 | @post('/boards')
69 | def admin_post_board(self, name, title, description):
70 | site = self.base.site
71 | if name in site.boards:
72 | self.base.error("A board with name %r already exists." % name)
73 | site.add_board(name, title, description)
74 | return self.response()
75 |
76 | raven = release = None
77 | def config(options):
78 | global raven, release
79 | raven = options.get('raven')
80 | release = options.get('release')
81 |
--------------------------------------------------------------------------------
/doc/try.rst:
--------------------------------------------------------------------------------
1 | =================
2 | Trying Valuenator
3 | =================
4 |
5 | .. _demo-label:
6 |
7 | Valuenator demo
8 | ===============
9 |
10 | The Valuenator demo is a version of Valuenator that stores its data
11 | locally in your browser. Data are stored persistently, so you can
12 | reload the demo page or restart your browser without losing data.
13 |
14 | The demo has all of the core features of the online version.
15 |
16 | The demo version has some limitations:
17 |
18 | - Because data are stored in your browser, you can't collaborate with
19 | other people. You also can't view data from multiple browsers.
20 |
21 | - Searching in the demo is a little different than in the online
22 | version. The demo searches for features and tasks using simple
23 | substring searches, while the on-line version uses a language-aware
24 | full-text search engine.
25 |
26 | - It requires a modern web browser. It is known to work with current
27 | versions of Chrome, Edge, Firefox, and Safari. Desktop browsers are
28 | required to drag tasks between states.
29 |
30 | To run the demo, open the following URL:
31 |
32 | http://valuenator.com/demo#/board/sample
33 |
34 | This will take you to a board with some sample features.
35 |
36 | See the :doc:`Valuenator documentation ` for information
37 | of using Valuenator.
38 |
39 | .. _beta-label:
40 |
41 | Valuenator beta
42 | ===============
43 |
44 | You can try the online version of Valuenator for free during the beta period.
45 |
46 | The goals of the beta are:
47 |
48 | - Get feedback
49 |
50 | - Find out what resources are needed to run Valuenator.
51 |
52 | To request beta access, fill out the `beta request form
53 | `_:
54 |
55 | https://goo.gl/forms/nxECJrBPCYB6WC6x2
56 |
57 | When we're ready, we'll get back to you with instructions for getting
58 | started.
59 |
60 | As with the demo version, the on-line beta requires a modern browser,
61 | such as Chrome, Edge, Firefox, or Safari.
62 |
63 | Docker image
64 | ============
65 |
66 | There's a `Valuenator docker image
67 | `_
68 | that you can use to easily deploy Valuenator yourself using Docker.
69 |
70 | Valuenator is open-source
71 | =========================
72 |
73 | Valuenator is `open source
74 | `_. You can check it
75 | out from github and run it yourself, although the easiest way to run
76 | Valuenator yourself is with the docker image.
77 |
78 |
79 |
--------------------------------------------------------------------------------
/client/ui/who.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Avatar from 'react-toolbox/lib/avatar';
3 | import Dropdown from 'react-toolbox/lib/dropdown';
4 | import Tooltip from 'react-toolbox/lib/tooltip';
5 | import md5 from 'md5';
6 |
7 | import {Dialog, DialogBase, Input} from './dialog';
8 |
9 | const TAvatar = Tooltip(Avatar);
10 |
11 |
12 | export const UserAvatar = (props) => {
13 | const {email, size, title} = props;
14 | const src = 'https://www.gravatar.com/avatar/' +
15 | md5(email) + '.jpg?s=' + (size || 32) + '&d=wavatar';
16 | if (title) {
17 | return ;
18 | }
19 | else {
20 | return ;
21 | }
22 | };
23 |
24 | export const User = (props) => (
25 |
161 |
162 | {this.details()}
163 | {this.tasks()}
164 | this.restore()}
167 | tooltip="Take this feature from the bag and work on it some more."
168 | />
169 |
170 |
208 | );
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Feature flow documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Jun 13 16:58:56 2017.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | # import os
20 | # import sys
21 | # sys.path.insert(0, os.path.abspath('.'))
22 |
23 |
24 | # -- General configuration ------------------------------------------------
25 |
26 | # If your documentation needs a minimal Sphinx version, state it here.
27 | #
28 | # needs_sphinx = '1.0'
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = []
34 |
35 | # Add any paths that contain templates here, relative to this directory.
36 | templates_path = ['_templates']
37 |
38 | # The suffix(es) of source filenames.
39 | # You can specify multiple suffix as a list of string:
40 | #
41 | # source_suffix = ['.rst', '.md']
42 | source_suffix = '.rst'
43 |
44 | # The master toctree document.
45 | master_doc = 'contents'
46 |
47 | # General information about the project.
48 | project = u'Feature flow'
49 | copyright = u'2017, Feature-flow contributors'
50 | author = u'Feature-flow contributors'
51 |
52 | # The version info for the project you're documenting, acts as replacement for
53 | # |version| and |release|, also used in various other places throughout the
54 | # built documents.
55 | #
56 | # The short X.Y version.
57 | version = u'0.1'
58 | # The full version, including alpha/beta/rc tags.
59 | release = u'0.1'
60 |
61 | # The language for content autogenerated by Sphinx. Refer to documentation
62 | # for a list of supported languages.
63 | #
64 | # This is also used if you do content translation via gettext catalogs.
65 | # Usually you set "language" from the command line for these cases.
66 | language = None
67 |
68 | # List of patterns, relative to source directory, that match files and
69 | # directories to ignore when looking for source files.
70 | # This patterns also effect to html_static_path and html_extra_path
71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
72 |
73 | # The name of the Pygments (syntax highlighting) style to use.
74 | pygments_style = 'sphinx'
75 |
76 | # If true, `todo` and `todoList` produce output, else they produce nothing.
77 | todo_include_todos = False
78 |
79 |
80 | # -- Options for HTML output ----------------------------------------------
81 |
82 | # The theme to use for HTML and HTML Help pages. See the documentation for
83 | # a list of builtin themes.
84 | #
85 | html_theme = 'default'
86 |
87 | # Theme options are theme-specific and customize the look and feel of a theme
88 | # further. For a list of options available for each theme, see the
89 | # documentation.
90 | #
91 | # html_theme_options = {}
92 |
93 | # Add any paths that contain custom static files (such as style sheets) here,
94 | # relative to this directory. They are copied after the builtin static files,
95 | # so a file named "default.css" will overwrite the builtin "default.css".
96 | html_static_path = ['_static']
97 |
98 |
99 | # -- Options for HTMLHelp output ------------------------------------------
100 |
101 | # Output file base name for HTML help builder.
102 | htmlhelp_basename = 'Featureflowdoc'
103 |
104 |
105 | # -- Options for LaTeX output ---------------------------------------------
106 |
107 | latex_elements = {
108 | # The paper size ('letterpaper' or 'a4paper').
109 | #
110 | # 'papersize': 'letterpaper',
111 |
112 | # The font size ('10pt', '11pt' or '12pt').
113 | #
114 | # 'pointsize': '10pt',
115 |
116 | # Additional stuff for the LaTeX preamble.
117 | #
118 | # 'preamble': '',
119 |
120 | # Latex figure (float) alignment
121 | #
122 | # 'figure_align': 'htbp',
123 | }
124 |
125 | # Grouping the document tree into LaTeX files. List of tuples
126 | # (source start file, target name, title,
127 | # author, documentclass [howto, manual, or own class]).
128 | latex_documents = [
129 | (master_doc, 'Featureflow.tex', u'Feature flow Documentation',
130 | u'Feature-flow contributors', 'manual'),
131 | ]
132 |
133 |
134 | # -- Options for manual page output ---------------------------------------
135 |
136 | # One entry per manual page. List of tuples
137 | # (source start file, name, description, authors, manual section).
138 | man_pages = [
139 | (master_doc, 'featureflow', u'Feature flow Documentation',
140 | [author], 1)
141 | ]
142 |
143 |
144 | # -- Options for Texinfo output -------------------------------------------
145 |
146 | # Grouping the document tree into Texinfo files. List of tuples
147 | # (source start file, target name, title, author,
148 | # dir menu entry, description, category)
149 | texinfo_documents = [
150 | (master_doc, 'Featureflow', u'Feature flow Documentation',
151 | author, 'Featureflow', 'One line description of project.',
152 | 'Miscellaneous'),
153 | ]
154 |
155 |
156 |
157 | # -- Options for Epub output ----------------------------------------------
158 |
159 | # Bibliographic Dublin Core info.
160 | epub_title = project
161 | epub_author = author
162 | epub_publisher = author
163 | epub_copyright = copyright
164 |
165 | # The unique identifier of the text. This can be a ISBN number
166 | # or the project homepage.
167 | #
168 | # epub_identifier = ''
169 |
170 | # A unique identification for the text.
171 | #
172 | # epub_uid = ''
173 |
174 | # A list of files that should not be packed into the epub file.
175 | epub_exclude_files = ['search.html']
176 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Feature flow
3 | ============
4 |
5 | Feature flow is an agile practice that seeks to provide value as
6 | quickly as possible by focusing teams on individual units of value,
7 | called "features". Ideally, a team works on one feature at a time
8 | [#tcboo]_.
9 |
10 | A feature remains a team focus until it's providing value. It's not
11 | enough, for example, for software to be checked in with tests
12 | passing. It must be in front of customers or otherwise providing
13 | stakeholder value.
14 |
15 | How it works
16 | ============
17 |
18 | .. image:: sample-board.png
19 | :width: 40em
20 |
21 | Feature flow uses a two-level board. At the top level, is a feature
22 | board, showing features in progress or in the product backlog. The
23 | feature board provides a high-level view so stakeholders can see what's
24 | in progress and prioritize the backlog.
25 |
26 | When a feature enters development, a task board is used for the
27 | feature. This is the low-level view.
28 |
29 | At any point in time, there's feature board and 0 or more task boards,
30 | however, there should usually be only one or two task boards.
31 |
32 | Core concepts
33 | =============
34 |
35 | Board
36 | A board provides a place for a team to track their work.
37 |
38 | Feature
39 | Features are units of value.
40 |
41 | We use the term *feature* rather than value to emphasize visibility.
42 |
43 | A feature will often encompass multiple stories, depending on the
44 | granularity of stories. A feature should be as small as possible
45 | and still provide value [#cd]_.
46 |
47 | Task
48 | Features are broken into tasks, so that work may be subdivided and
49 | that progress may be tracked. Features *can* be small enough to have
50 | a single task. That is a good thing, because it means that value
51 | can be recognized sooner, but typically features require multiple
52 | tasks.
53 |
54 | When a feature enters a development state, a task board for the
55 | feature is used, allowing the team to coordinate effort to
56 | drive the feature to completion.
57 |
58 | How it's implemented
59 | ====================
60 |
61 | Feature flow can be implemented in a number of ways:
62 |
63 | - You can implement feature flow with a feature board and a collection of task
64 | boards, either using a software tool or sticky-notes on physical boards.
65 |
66 | - You can use a single board with big cards for features and sticky
67 | notes for tasks. When a feature enters a development state, you can
68 | move the stickies between development task states.
69 |
70 | - Possibly using a `tool that supports complex workflows
71 | `_.
72 |
73 | - The :doc:`Valuenator ` application.
74 |
75 | The Valuenator `open source application
76 | `_ is an attempt to
77 | automate the practice in a simple and opinionated way. There's a
78 | :ref:`demo version ` you can
79 | try without installing or signing up for anything to get a feel for
80 | the mechanics of the practice.
81 |
82 | However it's implemented, it's important that the implementation makes
83 | it easy to see everything relevant to a team at once. This is one
84 | reason why we think that a more specialized and opinionated tool has
85 | value.
86 |
87 | How it fits in with other agile practices
88 | =========================================
89 |
90 | Like any other agile practice, feature flow is a part of a larger
91 | agile process that teams should tailor to their needs and experience
92 | through a process of "inspect and adapt". Just as software should be
93 | built incrementally, so should you evolve your agile processes
94 | incrementally. Feature flow is one part.
95 |
96 | Feature flow is an alternative to Scrum sprints. Rather than
97 | organizing work into fixed time increments, feature flow organizes
98 | around units of value. Features play a similar role to sprints,
99 | focusing a team on a shared goal and features are often similar in
100 | size to sprints.
101 |
102 | Organizing around value rather than time has a number of advantages:
103 |
104 | - It focuses the team on what's important to stakeholders.
105 |
106 | This may, for example, include activities outside of traditional
107 | development, such as deployment or training, because the team is
108 | focused on achieving value, not just finishing promised work.
109 |
110 | - It provides value as soon as possible, not just at sprint boundaries.
111 |
112 | - Much less time is spent in sprint planning, because there aren't sprints.
113 |
114 | - Team improvement can be considered at any time, rather than at
115 | sprint boundaries, because there's less emphasis on deadlines.
116 |
117 | Feature flow isn't new. Feature flow can be seen as an instance of
118 | `continuous flow
119 | `_,
120 | in that there's team focus on individual backlog items.
121 |
122 | Feature flow is based on two-tiered Kanban boards as described in the
123 | book `Kanban, by David Anderson
124 | `_ (and elsewhere).
125 |
126 | Feature flow can and should be used with other agile practices, as
127 | part of a larger process.
128 |
129 |
130 | .. [#tcboo] In practice, when a feature is nearing completion, there
131 | may not be enough work left to occupy the whole team, so the team
132 | may start another, however, the top priority of the team is getting
133 | the first task finished.
134 |
135 | .. [#cd] In a continuous-deployment environment, you might deploy
136 | subsets of features, with subsets not user-visible. This can help
137 | to avoid large software changes, to mitigate the risk of breakage.
138 | It can be argued that this provides value, but it's value that's
139 | not really visible to stake holders. Which isn't to say that
140 | feature flow and continuous deployment can't be used together, but
141 | they represent different kinds of flow.
142 |
--------------------------------------------------------------------------------
/client/ui/board.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classes from 'classnames';
3 |
4 | import {AddProject} from './project';
5 | import {Base} from './app';
6 | import {BoardAPI} from 'BoardAPI';
7 | import {Dialog, DialogBase, Input} from './dialog';
8 | import {Draggable, DropZone} from './dnd';
9 | import {Frame} from './frame';
10 | import {TheBag} from './thebag';
11 | import {TooltipButton, TooltipIconButton} from './util';
12 | import {Project} from './project';
13 |
14 | export class Board extends Base {
15 |
16 | new_api(props) {
17 | return new BoardAPI(this, props.params.name);
18 | }
19 |
20 | show_rename() {
21 | const name = this.state.model.name;
22 | this.refs.rename.show({old_name: name, name: name});
23 | }
24 |
25 | download() {
26 | this.api.export_url((url) => {
27 | window.open(url, '_blank');
28 | });
29 | }
30 |
31 | render() {
32 | const board = this.state.model;
33 | if (board.NotFound) {
34 | window.location.hash = '#/';
35 | }
36 | if (this.props.params.name != board.name) {
37 | window.location.hash = '#/board/' + encodeURIComponent(board.name);
38 | }
39 | document.title = board.name;
40 |
41 | const extra_nav = board.user.admin ? [
42 | ( this.download()}
45 | />),
46 | ( this.show_rename()}
49 | />)
50 | ] : null;
51 |
52 | return (
53 |