Download:
child 1:e2ac489d7045
0:a02e94c5b96b
Anton Shestakov <engored@ya.ru>, Sun, 16 Dec 2012 19:00:48 +0900
Made public.

15 файлов изменено, 812 вставок(+), 0 удалений(-) [+]
.hgignore file | annotate | diff | comparison | revisions
REQUIREMENTS file | annotate | diff | comparison | revisions
app.py file | annotate | diff | comparison | revisions
fruitbar/__init__.py file | annotate | diff | comparison | revisions
fruitbar/indexes.py file | annotate | diff | comparison | revisions
static/css/custom.css file | annotate | diff | comparison | revisions
static/favicon.ico file | annotate | diff | comparison | revisions
static/js/backbone.shard.js file | annotate | diff | comparison | revisions
static/js/framework/collections.js file | annotate | diff | comparison | revisions
static/js/framework/models.js file | annotate | diff | comparison | revisions
static/js/framework/views/base.js file | annotate | diff | comparison | revisions
static/js/framework/views/inline.js file | annotate | diff | comparison | revisions
static/js/framework/views/projects.js file | annotate | diff | comparison | revisions
static/js/framework/views/tasks.js file | annotate | diff | comparison | revisions
static/js/ui.js file | annotate | diff | comparison | revisions
templates/index.html file | annotate | diff | comparison | revisions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,5 @@
+syntax: glob
+*.pyc
+*.pyo
+venv/
+db/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/REQUIREMENTS Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,3 @@
+CodernityDB==0.3.63
+Flask==0.9
+Flask-RESTful==0.1.2
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app.py Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+import os
+
+from flask import Flask, g, request, render_template
+from flask.ext.restful import Resource, Api
+from CodernityDB.database import RecordNotFound
+from CodernityDB.database_thread_safe import ThreadSafeDatabase
+
+from fruitbar.indexes import ProjectIndex, TaskIndex
+
+
+app = Flask(__name__)
+api = Api(app)
+
+
+db_path = os.path.join(os.path.dirname(__file__), 'db')
+cdb = ThreadSafeDatabase(db_path)
+
+if cdb.exists():
+ cdb.open()
+ cdb.reindex()
+else:
+ cdb.create()
+
+ cdb.add_index(ProjectIndex(cdb.path, 'project'))
+ cdb.add_index(TaskIndex(cdb.path, 'task'))
+
+ cdb.insert({'_id': '29c21f00190f475ba2d855f810c1085e', '_t': 'project', 'name': 'Test Project', 'color': 'success'})
+ cdb.insert({'_t': 'task', 'project': '29c21f00190f475ba2d855f810c1085e', 'name': 'Test Task', 'note': 'Task Note', 'done': True})
+
+
+@app.before_request
+def before_request():
+ g.db = cdb
+
+
+class ResourceList(Resource):
+ db_index = 'id'
+ doc_stub = {}
+
+ def get(self):
+ return [project['doc'] for project in g.db.all(self.db_index, with_doc=True)]
+
+ def post(self):
+ doc = self.doc_stub.copy()
+ doc.update(request.json)
+
+ response = g.db.insert(doc)
+
+ return g.db.get('id', response['_id'], with_doc=True)
+
+
+class CRUDResource(Resource):
+ """ CRUD? More like RUD!
+ """
+
+ safe_fields = tuple()
+
+ def get(self, resource_id):
+ try:
+ doc = g.db.get('id', resource_id, with_doc=True)
+ except RecordNotFound:
+ return '', 404
+
+ return doc
+
+ def put(self, resource_id):
+ try:
+ doc = g.db.get('id', resource_id, with_doc=True)
+ except RecordNotFound:
+ return '', 404
+
+ userdata = dict((k, v) for (k, v) in request.json.items() if k in self.safe_fields)
+ doc.update(userdata)
+ response = g.db.update(doc)
+
+ return self.get(response['_id'])
+
+ def delete(self, resource_id):
+ try:
+ doc = g.db.get('id', resource_id, with_doc=True)
+ except RecordNotFound:
+ return '', 404
+
+ g.db.delete(doc)
+
+ return '', 200
+
+
+class ProjectList(ResourceList):
+ doc_stub = {'_t': 'project'}
+ db_index = 'project'
+
+
+class Project(CRUDResource):
+ safe_fields = ('name', 'color')
+
+ def delete(self, resource_id):
+ for task in g.db.get_many('task', resource_id, limit=-1, with_doc=True):
+ response = g.db.delete(task['doc'])
+
+ return super(Project, self).delete(resource_id)
+
+
+class TaskList(ResourceList):
+ doc_stub = {'_t': 'task'}
+ db_index = 'task'
+
+
+class Task(CRUDResource):
+ safe_fields = ('name', 'note', 'done')
+
+
+api.add_resource(ProjectList, '/projects/')
+api.add_resource(Project, '/projects/<resource_id>/')
+api.add_resource(TaskList, '/tasks/')
+api.add_resource(Task, '/tasks/<resource_id>/')
+
+
+@app.route('/')
+def index():
+ data = {
+ 'projects': [project['doc'] for project in g.db.all('project', with_doc=True)],
+ 'tasks': [task['doc'] for task in g.db.all('task', with_doc=True)]
+ }
+
+ return render_template('index.html', data=data)
+
+
+def main():
+ app.run(debug=True)
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fruitbar/indexes.py Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,31 @@
+#-*- coding:utf-8 -*-
+
+from hashlib import md5
+
+from CodernityDB.hash_index import HashIndex
+
+
+class ProjectIndex(HashIndex):
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '16s'
+ super(ProjectIndex, self).__init__(*args, **kwargs)
+
+ def make_key_value(self, data):
+ if data['_t'] == 'project':
+ return md5(data['name'].encode('utf-8')).digest(), None
+
+ def make_key(self, key):
+ return md5(key).digest()
+
+
+class TaskIndex(HashIndex):
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '16s'
+ super(TaskIndex, self).__init__(*args, **kwargs)
+
+ def make_key_value(self, data):
+ if data['_t'] == 'task':
+ return md5(data['project'].encode('utf-8')).digest(), None
+
+ def make_key(self, key):
+ return md5(key).digest()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/css/custom.css Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,60 @@
+.project {
+ padding-bottom: 20px;
+}
+
+.project h4 {
+ margin-top: 0;
+}
+
+.project .progress {
+ cursor: pointer;
+}
+
+.task .task-note {
+ font-size: 12px;
+ line-height: 15px;
+ padding: 2px 4px;
+ margin-bottom: 2px;
+ border-radius: 3px;
+}
+
+.task .task-note.present {
+ background-color: #f7f7f9;
+ border: 1px solid #e1e1e8;
+}
+
+.task input[type="checkbox"] {
+ float: left;
+ margin-right: 6px;
+}
+
+.row .ondemand {
+ visibility: hidden;
+}
+
+.row:hover .ondemand {
+ visibility: visible;
+}
+
+.inline-editable:hover {
+ color: #08c;
+ border-bottom: 1px dashed #08c;
+}
+
+.inline-editor {
+ position: relative;
+ margin-bottom: 0;
+}
+
+.inline-editor input[type="text"] {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.inline-editor textarea {
+ margin-bottom: 0;
+}
+
+.align-right {
+ text-align: right;
+}
Binary file static/favicon.ico has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/backbone.shard.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,79 @@
+/*
+ * backbone.shard.js.
+ *
+ * (c) 2012 Anton Shestakov.
+ *
+ * This extension to Backbone may be freely distributed
+ * under the MIT license:
+ * http://opensource.org/licenses/mit-license.php
+ */
+
+Backbone.Shard = function(options) {
+ this._collection = options.collection;
+ this._filter = options.filter;
+
+ this.models = this._collection.filter(this._filter);
+
+ this._collection
+ .on('all', function(event, model) {
+ if (event == 'change') {
+ if (this._filter(model) && _(this.models).contains(model)) {
+ this.trigger.apply(this, arguments);
+ }
+ if (this._filter(model) && !_(this.models).contains(model)) {
+ this.models.push(model);
+ this.trigger('add', model);
+ }
+ if (!this._filter(model) && _(this.models).contains(model)) {
+ this.models = _.without(this.models, model);
+ this.trigger('remove', model);
+ }
+ } else if (/^change:/.test(event) && this._filter(model) && _(this.models).contains(model)) {
+ this.trigger.apply(this, arguments);
+ }
+ }, this)
+ .on('add', function(model) {
+ if (this._filter(model) && !_(this.models).contains(model)) {
+ this.models.push(model);
+ this.trigger('add', model);
+ }
+ }, this)
+ .on('remove', function(model) {
+ if (this._filter(model) && _(this.models).contains(model)) {
+ this.models = _.without(this.models, model);
+ this.trigger('remove', model);
+ }
+ }, this)
+ .on('reset', function() {
+ this.models = this._collection.filter(this._filter);
+ this.trigger('reset');
+ }, this);
+
+ this.initialize.apply(this, arguments);
+};
+
+_.extend(Backbone.Shard.prototype, Backbone.Events, {
+ initialize: function() {},
+
+ pluck: function(attr) {
+ return _.map(this.models, function(model) { return model.get(attr); });
+ },
+
+ chain: function () {
+ return _(this.models).chain();
+ }
+});
+
+// Underscore methods that we want to implement on the Shard.
+var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
+ 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
+ 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
+ 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
+ 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
+
+// Mix in each Underscore method as a proxy to `Shard#models`.
+_.each(methods, function(method) {
+ Backbone.Shard.prototype[method] = function() {
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
+ };
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/collections.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,9 @@
+var Projects = Backbone.Collection.extend({
+ model: Project,
+ url: '/projects/'
+});
+
+var Tasks = Backbone.Collection.extend({
+ model: Task,
+ url: '/tasks/'
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/models.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,37 @@
+var Model = Backbone.Model.extend({
+ url: function() {
+ var url = Backbone.Model.prototype.url.call(this);
+ return url[url.length - 1] == '/' ? url : url + '/';
+ },
+
+ adhoc: function(data) {
+ var model = this;
+ var options = {
+ url: this.urlRoot + encodeURIComponent(this.id) + '/',
+ contentType: 'application/json',
+ data: JSON.stringify(data)
+ };
+
+ return (this.sync || Backbone.sync).call(this, 'update', model, options).then(function(serverAttrs) {
+ model.set(serverAttrs);
+ });
+ }
+});
+
+var Project = Model.extend({
+ idAttribute: '_id',
+ urlRoot: '/projects/',
+
+ switchColor: function() {
+ var colorChain = [null, 'info', 'success', 'warning', 'danger'];
+ var currentColorIndex = colorChain.indexOf(this.get('color') || null);
+ var nextColor = colorChain[(currentColorIndex + 1) % colorChain.length];
+
+ return this.adhoc({color: nextColor});
+ }
+});
+
+var Task = Model.extend({
+ idAttribute: '_id',
+ urlRoot: '/tasks/'
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/views/base.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,44 @@
+var CollectionView = Backbone.View.extend({
+ appendItem: function() {},
+ updateItem: function() {},
+ removeItem: function() {},
+ clear: function() {},
+ repopulate: function() {
+ this.clear();
+ this.collection.each(this.appendItem, this);
+ },
+ bindCollection: function() {
+ this.repopulate();
+
+ this.collection
+ .on('add', this.appendItem, this)
+ .on('change', this.updateItem, this)
+ .on('remove', this.removeItem, this)
+ .on('reset', this.repopulate, this);
+ },
+ unbindCollection: function() {
+ this.collection.off(null, null, this);
+ },
+ compileTemplates: function(templates) {
+ this.templates = {};
+
+ _.each(templates, function(template, name) {
+ this.templates[name] = _.template(template);
+ }, this);
+ }
+});
+
+var CollectionCounterView = CollectionView.extend({
+ initialize: function(options) {
+ this.bindCollection();
+ },
+ appendItem: function() {
+ this.repopulate();
+ },
+ removeItem: function() {
+ this.repopulate();
+ },
+ repopulate: function() {
+ this.$el.html(this.collection.models.length);
+ }
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/views/inline.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,105 @@
+var InlineEditorView = Backbone.View.extend({
+ initialize: function(options) {
+ this.render(options);
+ },
+
+ renderInput: function(options) {
+ var view = this;
+
+ this.$input = $('<input>').attr('type', 'text').addClass('input-xlarge').val(options.target.text());
+
+ this.$input.keyup(function(e) {
+ switch (e.keyCode) {
+ case 13:
+ view.$saveButton.click();
+ break;
+ case 27:
+ view.$cancelButton.click();
+ break;
+ }
+ });
+
+ this.$el.append(this.$input, this.$saveButton, this.$cancelButton);
+ options.target.after(this.$el);
+ this.$input.focus();
+
+ return this;
+ },
+
+ renderTextarea: function(options) {
+ var view = this;
+
+ this.$input = $('<textarea>').addClass('input-xlarge').attr('rows', 3).text(options.target.text());
+
+ this.$input.keyup(function(e) {
+ switch (e.keyCode) {
+ case 13:
+ if (e.ctrlKey) {
+ view.$saveButton.click();
+ }
+ break;
+ case 27:
+ view.$cancelButton.click();
+ break;
+ }
+ });
+
+ this.$el.append(this.$input, this.$saveButton, this.$cancelButton);
+ options.target.after(this.$el);
+ this.$input.focus();
+
+ return this;
+ },
+
+ render: function(options) {
+ var view = this;
+
+ this.$saveButton = $('<button>').addClass('btn btn-small btn-success').html('<i class="icon-white icon-ok"></i>');
+ this.$cancelButton = $('<button>').addClass('btn btn-small').html('<i class="icon-remove"></i>');
+ this.$el = $('<div>').addClass('inline-editor input-append');
+
+ this.$cancelButton.click(function() {
+ view.$el.remove();
+ options.target.show();
+ });
+
+ this.$saveButton.click(function() {
+ view.trigger('save', view.$input.val());
+ });
+
+ switch (options.type) {
+ case 'input':
+ return this.renderInput(options);
+ case 'textarea':
+ return this.renderTextarea(options);
+ }
+ }
+});
+
+
+var CollectionViewWithInlineEditor = CollectionView.extend({
+ bindInlineEditable: function(model, selector) {
+ model.$item.delegate(selector, 'click', function(e) {
+ e.preventDefault();
+
+ var $this = $(this);
+ var attribute = $this.attr('data-model-attribute');
+ var inlineEditor = new InlineEditorView({
+ target: $this,
+ type: $this.attr('data-input-type') || 'input'
+ });
+
+ $this.hide();
+
+ inlineEditor.on('save', function(value) {
+ var data = {};
+ data[attribute] = value;
+
+ model.adhoc(data).then(function() {
+ inlineEditor.remove();
+ $this.show();
+ });
+ });
+ });
+ }
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/views/projects.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,101 @@
+var ProjectsView = CollectionViewWithInlineEditor.extend({
+ initialize: function(options) {
+ this.tasks = options.tasks;
+ this.rawTemplates = options.templates;
+
+ this.compileTemplates(options.templates);
+ this.bindCollection();
+
+ options.controls.create.click(function() {
+ options.collection.create({
+ name: 'new project'
+ }, {wait: true});
+ });
+ },
+
+ clear: function() {
+ this.$el.empty();
+ },
+
+ renderItem: function(project) {
+ var $item = $(this.templates.project(project.toJSON()));
+
+ $item.tasksView = new TasksView({
+ el: $item.find('.tasks'),
+ collection: project.tasks,
+ templates: this.rawTemplates,
+ controls: {
+ create: $item.find('.btn-new-task')
+ }
+ });
+
+ $item
+ .find('.progress')
+ .click(function() {
+ var $progress = $(this);
+ $progress.addClass('active');
+ project.switchColor().then(function() {
+ $progress.removeClass('active');
+ });
+ });
+
+ $item
+ .find('.btn-delete-project')
+ .click(function() {
+ project.destroy({wait: true});
+ });
+
+ return $item;
+ },
+
+ updateProgress: function(project) {
+ project.$item
+ .find('.progress')
+ .removeClass('progress-info progress-success progress-warning progress-danger')
+ .addClass(project.get('color') ? 'progress-' + project.get('color') : '')
+ .find('.bar')
+ .css('width', (100 * project.tasks.progress()) + '%');
+ },
+
+ appendItem: function(project) {
+ var tasks = this.tasks;
+
+ project.tasks = new Backbone.Shard({
+ collection: this.tasks,
+ filter: function(task) { return task.get('project') === project.id; }
+ });
+
+ project.tasks.progress = function() {
+ return this.filter(function(task) { return task.get('done') === true; }).length / this.size();
+ };
+
+ project.tasks.create = function(attributes, options) {
+ attributes = attributes || {};
+ attributes['project'] = project.id;
+ return tasks.create(attributes, options);
+ };
+
+ project.$item = this.renderItem(project);
+
+ this.bindInlineEditable(project, '.inline-editable[data-model="project"]');
+ this.updateProgress(project);
+
+ project.on('change:name', function() {
+ project.$item.find('[data-model="project"][data-model-attribute="name"]').text(project.get('name'));
+ });
+
+ project.on('change:color', function() {
+ this.updateProgress(project);
+ }, this);
+
+ project.tasks.on('add remove change:done', function() {
+ this.updateProgress(project);
+ }, this);
+
+ this.$el.append(project.$item);
+ },
+
+ removeItem: function(project) {
+ project.$item.remove();
+ }
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/framework/views/tasks.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,71 @@
+var TasksView = CollectionViewWithInlineEditor.extend({
+ initialize: function(options) {
+ this.compileTemplates(options.templates);
+ this.bindCollection();
+
+ options.controls.create.click(function() {
+ options.collection.create({
+ name: 'new task'
+ }, {wait: true});
+ });
+ },
+
+ clear: function() {
+ this.$el.empty();
+ },
+
+ renderItem: function(task) {
+ var $item = $(this.templates.task(task.toJSON()));
+
+ $item
+ .find('input:checkbox')
+ .change(function() {
+ task.adhoc({'done': this.checked});
+ });
+
+ $item
+ .find('.btn-delete-task')
+ .click(function() {
+ task.destroy({wait: true});
+ });
+
+ return $item;
+ },
+
+ updateNote: function(task) {
+ var $note = task.$item.find('[data-model="task"][data-model-attribute="note"]');
+
+ if (task.get('note')) {
+ $note.text(task.get('note'));
+ task.$item.find('.task-note').removeClass('muted').addClass('present');
+ } else {
+ $note.text('...');
+ task.$item.find('.task-note').addClass('muted').removeClass('present');
+ }
+ },
+
+ appendItem: function(task) {
+ task.$item = this.renderItem(task);
+
+ this.bindInlineEditable(task, '.inline-editable[data-model="task"]');
+ this.updateNote(task);
+
+ task.on('change:done', function() {
+ task.$item.find('input:checkbox').prop('checked', task.get('done'));
+ });
+
+ task.on('change:name', function() {
+ task.$item.find('[data-model="task"][data-model-attribute="name"]').text(task.get('name'));
+ });
+
+ task.on('change:note', function() {
+ this.updateNote(task);
+ }, this);
+
+ this.$el.append(task.$item);
+ },
+
+ removeItem: function(task) {
+ task.$item.remove();
+ }
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/ui.js Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,26 @@
+ $(function() {
+ window.fruitbar = {};
+ fruitbar.tasks = new Tasks(data['tasks']);
+ fruitbar.projects = new Projects(data['projects']);
+
+ fruitbar.projectCounter = new CollectionCounterView({
+ el: $('.project-counter'),
+ collection: fruitbar.projects
+ });
+
+ fruitbar.projectsView = new ProjectsView({
+ el: $('.projects'),
+ collection: fruitbar.projects,
+ tasks: fruitbar.tasks,
+ templates: {
+ project: $('#project-template').html(),
+ task: $('#task-template').html()
+ },
+ controls: {
+ create: $('.btn-new-project')
+ }
+ });
+
+ window.setInterval(function() {fruitbar.projects.fetch({update: true});}, 60000);
+ window.setInterval(function() {fruitbar.tasks.fetch({update: true});}, 60000);
+});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/index.html Sun Dec 16 19:00:48 2012 +0900
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Fruit Bar Progress Tracker</title>
+ <link type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}" rel="shortcut icon">
+
+ <link type="text/css" href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.1/css/bootstrap-combined.min.css" rel="stylesheet">
+ <link type="text/css" href="{{ url_for('static', filename='css/custom.css') }}" rel="stylesheet">
+
+ <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
+ <script type="text/javascript" src="//underscorejs.org/underscore-min.js"></script>
+ <script type="text/javascript" src="//backbonejs.org/backbone-min.js"></script>
+ <script type="text/javascript" src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.1/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/backbone.shard.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/models.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/collections.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/views/base.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/views/inline.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/views/projects.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/framework/views/tasks.js') }}"></script>
+ <script type="text/javascript" src="{{ url_for('static', filename='js/ui.js') }}"></script>
+
+ <script type="text/javascript">
+ _.extend(_.templateSettings, {
+ variable: 'data'
+ });
+ </script>
+ </head>
+ <body>
+ <div class="container">
+ <div class="row">
+ <div class="span2">
+ <h3 class="align-right">
+ <button class="btn btn-mini btn-success btn-new-project">New project</button>
+ </h3>
+ </div>
+ <div class="span8">
+ <h3 class="muted">
+ All projects <small>(<span class="project-counter"></span>)</small>
+ </h3>
+ <hr>
+ </div>
+ </div>
+
+ <section class="projects">
+ <script id="project-template" type="text/template">
+ <div class="project">
+ <div class="row">
+ <div class="span2 align-right ondemand">
+ <button class="btn btn-mini btn-danger btn-delete-project">Delete</button>
+ <button class="btn btn-mini btn-success btn-new-task">New task</button>
+ </div>
+ <div class="span4">
+ <h4>
+ <span class="inline-editable"
+ data-input-type="input"
+ data-model="project"
+ data-model-attribute="name"><%- data.name %></span>
+ </h4>
+ </div>
+
+ <div class="span4">
+ <div class="progress progress-striped">
+ <div class="bar"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="tasks"></div>
+ </div>
+ </script>
+
+ <script id="task-template" type="text/template">
+ <div class="task">
+ <div class="row">
+ <div class="span2 align-right ondemand">
+ <button class="btn btn-mini btn-danger btn-delete-task">Delete</button>
+ </div>
+ <div class="span4">
+ <input type="checkbox" <% if (data.done) print('checked') %>>
+ <span class="inline-editable"
+ data-input-type="input"
+ data-model="task"
+ data-model-attribute="name"><%- data.name %></span>
+ </div>
+ <div class="span4">
+ <div class="task-note">
+ <span class="inline-editable"
+ data-input-type="textarea"
+ data-model="task"
+ data-model-attribute="note"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </script>
+ </section>
+ </div>
+
+ <script type="text/javascript">
+ var data = {{ data|tojson|safe }};
+ </script>
+ </body>
+</html>