twdeps 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .cache_rake_t
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+
6
+ before_script:
7
+ - mkdir ~/.task
8
+ - echo data.location=~/.task > ~/.taskrc
9
+ - task count
10
+
11
+ before_install:
12
+ - echo | sudo add-apt-repository ppa:ultrafredde/ppa
13
+ - sudo apt-get update
14
+ - sudo apt-get install task
15
+ - sudo apt-get install graphviz
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in twdeps.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :test, :test_paths => ['test/unit', 'test/integration'] do
7
+ watch('lib/twdeps.rb'){"test"}
8
+ watch(%r{^lib/twdeps/(.+)\.rb$}){|m| "test/unit/test_#{m[1]}.rb"}
9
+ watch(%r{^test/unit/test_(.+)\.rb$})
10
+ watch('test/test_helper.rb'){"test"}
11
+ watch('test/helpers/**/*'){"test"}
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Nicholas E. Rabenau
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # TaskWarrior Dependency Visualization
2
+
3
+ Visualizes dependencies between TaskWarrior tasks.
4
+
5
+ [![Build Status](https://secure.travis-ci.org/nerab/twdeps.png?branch=master)](http://travis-ci.org/nerab/twdeps)
6
+
7
+ ## Example
8
+
9
+ Given a set of interdependent tasks described in the TaskWarrior [tutorial](http://taskwarrior.org/projects/taskwarrior/wiki/Tutorial2#DEPENDENCIES), the tasks are
10
+
11
+ 1. Exported from TaskWarrior as JSON, then
12
+ 1. Piped into `twdeps`, and finally
13
+ 1. The output is directed to a PNG file.
14
+
15
+ Result:
16
+
17
+ ![party](/nerab/twdeps/raw/master/examples/party.png)
18
+
19
+ ## Installation
20
+
21
+ $ gem install twdeps
22
+
23
+ ## Usage
24
+
25
+ # Create a dependency graph as PNG and pipe it to a file
26
+ # See [Limitations](Limitations) below for why we need the extra task parms
27
+ task export rc.json.array=on rc.verbose=nothing | twdeps > deps.png
28
+
29
+ # Same but spefify output format
30
+ task export | twdeps --format svg > deps.svg
31
+
32
+ # Create a graph from a previously exported file
33
+ task export > tasks.json
34
+ cat tasks.json | twdeps > deps.png
35
+
36
+ # Display graph in browser without creating an intermediate file
37
+ # Requires bcat to be installed
38
+ task export | twdeps --format svg | bcat
39
+
40
+ ## Dependencies
41
+
42
+ The graph is generated with [ruby-graphviz](https://github.com/glejeune/Ruby-Graphviz), which in turn requires a local [Graphviz](http://graphviz.org/) installation (e.g. `brew install graphviz`).
43
+
44
+ [bcat](http://rtomayko.github.com/bcat/) is required for piping into a browser.
45
+
46
+ ## Limitations
47
+
48
+ Due to [two](http://taskwarrior.org/issues/1017) [bugs](http://taskwarrior.org/issues/1013) in JSON export, TaskWarrior 2.0 needs the command line options `rc.json.array=on` and `rc.verbose=nothing`.
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test' << 'test/helpers'
7
+ test.test_files = FileList['test/**/test_*.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/twdeps ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+ Bundler.require
5
+
6
+ require 'trollop'
7
+
8
+ def log(msg)
9
+ STDERR.puts "#{File.basename($0)}: #{msg}"
10
+ end
11
+
12
+ def die(msg = nil)
13
+ log(msg) unless msg.nil?
14
+ exit 1
15
+ end
16
+
17
+ opts = Trollop::options do
18
+ version "#{File.basename($0)} v#{TaskWarrior::Dependencies::VERSION} (c) 2012 Nicolas E. Rabenau"
19
+ banner <<-EOS
20
+ Visualizes dependencies between TaskWarrior tasks.
21
+
22
+ Usage:
23
+ #{File.basename($0)} [options]
24
+
25
+ where [options] are:
26
+
27
+ EOS
28
+ opt :format, "Specify output format", :default => 'svg'
29
+ opt :trace, "Enable trace output", :default => false
30
+ end
31
+
32
+ include TaskWarrior
33
+ include TaskWarrior::Dependencies
34
+
35
+ Trollop::die :format, "must be one of #{Graph.formats.join(', ')}" unless Graph.formats.include?(opts[:format])
36
+
37
+ begin
38
+ repo = Repository.new(ARGF.read)
39
+ master = Graph.new(File.basename($0)) # TODO Move this to a CommandlinePresenter
40
+
41
+ # Add all projects (will add their tasks and dependencies recursively)
42
+ repo.projects.each do |project|
43
+ master << project
44
+ end
45
+
46
+ # Add all project-less tasks as toplevel nodes
47
+ repo.tasks.reject{|t| t.project}.each do |task|
48
+ master << task
49
+ end
50
+
51
+ puts master.render(opts[:format])
52
+ rescue
53
+ if opts[:trace]
54
+ log($!)
55
+ $@.each{|line| log(line)}
56
+ else
57
+ die($!)
58
+ end
59
+ end
Binary file
@@ -0,0 +1,111 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ class NullPresenter
4
+ def attributes
5
+ {:label => 'Unknown', :fontcolor => 'red'}
6
+ end
7
+
8
+ def id
9
+ 'null'
10
+ end
11
+ end
12
+
13
+ # Builds a dependency graph
14
+ #
15
+ # +thing+ is added as node with all of its dependencies. A presenter is used to present the task as node label.
16
+ # +thing.id.to_s+ is called for the identifier. It must be unique within the graph and all of its dependencies.
17
+ #
18
+ # +thing.dependencies(thing)+ is called if +thing+ responds to it. It is expected to return a list
19
+ # of things the thing depends on. Each thing may have its own dependencies which will be resolved recursively.
20
+ #
21
+ # Design influenced by https://github.com/glejeune/Ruby-Graphviz/blob/852ee119e4e9850f682f0a0089285c36ee16280f/bin/gem2gv
22
+ #
23
+ class Graph
24
+ class << self
25
+ def formats
26
+ Constants::FORMATS
27
+ end
28
+ end
29
+
30
+ #
31
+ # Build a new Graph for +thing+
32
+ # # TODO Accept a presenter that would default to GlobalPresenter with {:rankdir => 'BT'}
33
+ def initialize(name = :G, attributes = [])
34
+ @graph = GraphViz::new(name, attributes)
35
+ @dependencies = []
36
+ @edges = []
37
+ end
38
+
39
+ def <<(task_or_project)
40
+ if task_or_project.respond_to?(:dependencies)
41
+ task = task_or_project
42
+ nodeA = find_or_create_node(task)
43
+ create_edges(nodeA, task.dependencies)
44
+
45
+ # resolve all dependencies we don't know yet
46
+ task.dependencies.each do |dependency|
47
+ unless @dependencies.include?(dependency)
48
+ @dependencies << dependency
49
+ self << dependency
50
+ end
51
+ end
52
+ else
53
+ # it's a project
54
+ project = task_or_project
55
+ cluster = Graph.new(presenter(project).id, presenter(project).attributes)
56
+
57
+ project.tasks.each do |task|
58
+ cluster << task
59
+ end
60
+
61
+ # add all nodes and edges from cluster as a subgraph to @graph
62
+ @graph.add_graph(cluster.graph)
63
+ end
64
+ end
65
+
66
+ def render(format)
67
+ @graph.output(format => String)
68
+ end
69
+
70
+ protected
71
+ attr_reader :graph
72
+
73
+ private
74
+ def create_edges(nodeA, nodes)
75
+ nodes.each do |node|
76
+ nodeB = find_or_create_node(node)
77
+ create_edge(nodeA, nodeB)
78
+ end
79
+ end
80
+
81
+ def find_or_create_node(thing)
82
+ @graph.get_node(presenter(thing).id) || create_node(thing)
83
+ end
84
+
85
+ def create_node(thing)
86
+ @graph.add_nodes(presenter(thing).id, presenter(thing).attributes)
87
+ end
88
+
89
+ def create_edge(nodeA, nodeB)
90
+ edge = [nodeA, nodeB]
91
+ unless @edges.include?(edge) # GraphViz lacks get_edge, so we need to track existing edges ourselfes
92
+ @edges << edge
93
+ @graph.add_edges(nodeA, nodeB)
94
+ end
95
+ end
96
+
97
+ def presenter(thing)
98
+ # TODO Will counter-caching the presenters improve performance?
99
+ if thing.nil?
100
+ NullPresenter.new
101
+ else
102
+ if thing.respond_to?(:dependencies)
103
+ TaskPresenter.new(thing)
104
+ else
105
+ ProjectPresenter.new(thing)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,9 @@
1
+ module TaskWarrior
2
+ class PriorityMapper
3
+ class << self
4
+ def map(json)
5
+ {'H' => :high, 'M' => :medium, 'L' => :low}[json]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Project
5
+ attr_reader :name, :tasks
6
+
7
+ include ActiveModel::Validations
8
+ validates :name, :presence => true
9
+ validate :name_may_not_contain_spaces
10
+
11
+ def initialize(name, tasks = [])
12
+ @name = name
13
+ @tasks = tasks
14
+ @tasks.each{|t| t.project = self}
15
+ end
16
+
17
+ def <<(task)
18
+ @tasks << task
19
+ task.project = self
20
+ end
21
+
22
+ def to_s
23
+ "Project #{name} (#{@tasks.size} tasks)"
24
+ end
25
+
26
+ private
27
+ def name_may_not_contain_spaces
28
+ if !name.blank? and name[/\s/]
29
+ errors.add(:name, "may not contain spaces")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ #
4
+ # Presents a project's attributes suitable for a GraphViz cluster
5
+ #
6
+ class ProjectPresenter
7
+ def initialize(project)
8
+ @project = project
9
+ end
10
+
11
+ def attributes
12
+ {:label => @project.name}
13
+ end
14
+
15
+ def id
16
+ "cluster_#{@project.name}"
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/twdeps/tag.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Tag
5
+ attr_reader :name
6
+
7
+ include ActiveModel::Validations
8
+ validates :name, :presence => true
9
+ validate :name_may_not_contain_spaces
10
+
11
+ def initialize(tag_or_name, tasks = [])
12
+ if tag_or_name.respond_to?(:name)
13
+ @name = tag_or_name.name
14
+ @tasks = tag_or_name.tasks
15
+ else
16
+ @name = tag_or_name
17
+ @tasks = []
18
+ end
19
+
20
+ tasks.each{|task|
21
+ self << task
22
+ }
23
+ end
24
+
25
+ def <<(task)
26
+ @tasks << task unless @tasks.include?(task)
27
+ end
28
+
29
+ def tasks
30
+ @tasks #.dup
31
+ end
32
+
33
+ def to_s
34
+ "Tag: #{name} (#{@tasks.size} tasks)"
35
+ end
36
+
37
+ def ==(other)
38
+ name == other.name
39
+ end
40
+
41
+ private
42
+ def name_may_not_contain_spaces
43
+ if !name.blank? and name[/\s/]
44
+ errors.add(:name, "may not contain spaces")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_model'
2
+
3
+ module TaskWarrior
4
+ class Task
5
+ attr_accessor :description, :id, :entry, :status, :uuid, :project, :dependencies, :parent, :children, :priority
6
+
7
+ include ActiveModel::Validations
8
+ validates :description, :id, :entry, :status, :uuid, :presence => true
9
+
10
+ validates :id, :numericality => { :only_integer => true, :greater_than => 0}
11
+
12
+ validates :uuid, :format => {:with => /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/,
13
+ :message => "'%{value}' does not match the expected format of a UUID"}
14
+
15
+ validates :status, :inclusion => {:in => [:pending, :waiting, :complete], :message => "%{value} is not a valid status"}
16
+
17
+ validates :priority, :inclusion => {
18
+ :in => [:high, :medium, :low],
19
+ :allow_nil => true,
20
+ :allow_blank => true,
21
+ :message => "%{value} is not a valid priority"
22
+ }
23
+
24
+ validate :entry_cannot_be_in_the_future
25
+
26
+ def initialize(description)
27
+ @description = description
28
+ @dependencies = []
29
+ @children = []
30
+ @tags = []
31
+ end
32
+
33
+ def tags
34
+ @tags
35
+ end
36
+
37
+ def to_s
38
+ "Task '#{description}'".tap{|result| result << " <#{uuid}>" if uuid}
39
+ end
40
+
41
+ private
42
+ def entry_cannot_be_in_the_future
43
+ begin
44
+ if !entry.blank? and entry > DateTime.now
45
+ errors.add(:entry, "can't be in the future")
46
+ end
47
+ rescue
48
+ errors.add(:entry, "must be comparable to DateTime")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ module TaskWarrior
2
+ #
3
+ # A DataMapper that makes new Tasks from a JSON representation
4
+ #
5
+ class TaskMapper
6
+ class << self
7
+ def map(json)
8
+ Task.new(json['description']).tap{|t|
9
+ t.id = json['id'].to_i
10
+ t.uuid = json['uuid']
11
+ t.entry = DateTime.parse(json['entry'])
12
+ t.status = json['status'].to_sym
13
+ t.project = json['project']
14
+
15
+ if json['depends']
16
+ if json['depends'].respond_to?(:split)
17
+ t.dependencies = json['depends'].split(',')
18
+ else
19
+ t.dependencies = json['depends']
20
+ end
21
+ end
22
+
23
+ t.parent = json['parent'] # Children will be cross-indexed in the repository
24
+ t.priority = PriorityMapper.map(json['priority'])
25
+ json['tags'].each{|tag| t.tags << tag} if json['tags']
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ #
4
+ # Presents a task's attributes suitable for a GraphViz node
5
+ #
6
+ class TaskPresenter
7
+ def initialize(task)
8
+ @task = task
9
+ end
10
+
11
+ def attributes
12
+ attrs = {:label => @task.description}
13
+ attrs.merge!({:tooltip => "Status: #{@task.status}"})
14
+
15
+ # TODO Once we see the urgency in the JSON export, we can color-code the nodes
16
+ # http://taskwarrior.org/issues/973
17
+ # attrs.merge!({:fillcolor => 'red', :style => 'filled'})
18
+
19
+ attrs.merge!({:fontcolor => 'gray', :color => 'gray'}) if :completed == @task.status
20
+ attrs
21
+ end
22
+
23
+ def id
24
+ @task.uuid
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ require 'json'
2
+
3
+ module TaskWarrior
4
+ class SimpleTag
5
+ attr_reader :name, :tasks
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @tasks = []
10
+ end
11
+
12
+ def <<(task)
13
+ @tasks << task unless @tasks.include?(task)
14
+ end
15
+ end
16
+
17
+ class Repository
18
+ def initialize(input)
19
+ @tasks = {}
20
+ @projects = Hash.new{|hash, key| hash[key] = Project.new(key)}
21
+ @tags = Hash.new{|hash, key| hash[key] = Tag.new(key)}
22
+
23
+ JSON.parse(input).each{|json|
24
+ task = TaskWarrior::TaskMapper.map(json)
25
+ @tasks[task.uuid] = task
26
+ @projects[task.project].tasks << task if task.project
27
+
28
+ # Create a new Tag object in @tags that is the value for each tag name
29
+ task.tags.each{|tag_name| @tags[tag_name] << task}
30
+ }
31
+
32
+ # Replace the uuid of each dependency with the real task
33
+ @tasks.each_value{|task| task.dependencies.map!{|uuid| @tasks[uuid]}}
34
+
35
+ # Replace the project property of each task with a proper Project object carrying a name and all of the project's tasks
36
+ @tasks.each_value{|task| task.project = @projects[task.project] if task.project}
37
+
38
+ # Add child tasks to their parent, but keep them in the global index
39
+ @tasks.each_value do |task|
40
+ if task.parent
41
+ parent = @tasks[task.parent]
42
+
43
+ if parent # we know the parent
44
+ parent.children << task
45
+ task.parent = parent
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def tasks
52
+ # Do not expose child tasks directly
53
+ @tasks.values.reject{|t| t.parent}
54
+ end
55
+
56
+ # direct lookup by uuid
57
+ def [](uuid)
58
+ @tasks[uuid]
59
+ end
60
+
61
+ def projects
62
+ @projects.values
63
+ end
64
+
65
+ def project(name)
66
+ @projects[name] if @projects.has_key?(name)
67
+ end
68
+
69
+ def tags
70
+ @tags.values
71
+ end
72
+
73
+ def tag(name)
74
+ @tags[name] if @tags.has_key?(name)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ module TaskWarrior
2
+ module Dependencies
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
data/lib/twdeps.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "twdeps/version"
2
+
3
+ require "twdeps/task"
4
+ require "twdeps/project"
5
+ require "twdeps/tag"
6
+
7
+ require "twdeps/task_mapper"
8
+ require "twdeps/priority_mapper"
9
+ require "twdeps/task_repository"
10
+
11
+ require "twdeps/graph"
12
+ require "twdeps/task_presenter"
13
+ require "twdeps/project_presenter"
14
+
15
+ require "graphviz"
16
+ require "json"
17
+
18
+ module TaskWarrior
19
+ module Dependencies
20
+ # Your code goes here...
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ [{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
2
+ {"id":2,"description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
3
+ {"id":3,"description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
4
+ {"id":4,"description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
5
+ {"id":5,"description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
6
+ {"id":6,"description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
7
+ ]
@@ -0,0 +1,7 @@
1
+ [{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
2
+ {"id":2,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
3
+ {"id":3,"depends":"9f6f3738-1c08-4f45-8eb4-1e90864c7588","description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
4
+ {"id":4,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a,c992448a-f1ea-4982-8461-47f0705ff509","description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
5
+ {"id":5,"depends":"c992448a-f1ea-4982-8461-47f0705ff509","description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
6
+ {"id":6,"depends":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a","description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
7
+ ]