omniboard 1.1.1

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.
@@ -0,0 +1,17 @@
1
+ Omniboard::Column.new "Active" do
2
+ order 1
3
+ conditions{ |p| p.active? }
4
+
5
+ width 2
6
+ columns 4
7
+
8
+ display :full
9
+
10
+ icon do |p|
11
+ if p.incomplete_tasks.size == 0
12
+ "svg:hanging"
13
+ elsif p.actionable_tasks.all?{ |t| t.context && t.context.name == "Waiting for..." }
14
+ "svg:waiting-on"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ Omniboard::Column.new "Backburner" do
2
+ order 0
3
+ conditions{ |p| p.on_hold? || p.deferred? }
4
+ display :compact
5
+ end
@@ -0,0 +1,5 @@
1
+ Omniboard::Column.new "Completed" do
2
+ order 2
3
+ conditions{ |p| p.completed? }
4
+ display :compact
5
+ end
@@ -0,0 +1,11 @@
1
+ # Place global config variables here. See the README for config variables
2
+ Omniboard::Column.config do
3
+ # Group projects by their containing folders
4
+ group_by{ |p| p.container || "" }
5
+
6
+ # Sort by rank, with top level projects at the top
7
+ sort_groups{ |c| c.nil? ? 0 : c.rank }
8
+
9
+ # Name based on ancestry
10
+ group_name{ |c| c.nil? ? "Top level" : c.ancestry.map(&:name).reverse.join("→")}
11
+ end
@@ -0,0 +1,22 @@
1
+ class String
2
+ HTML_ESCAPE_CHARS = {
3
+ "&" => "&",
4
+ ">" => ">",
5
+ "<" => "&lt;",
6
+ '"' => "&quot;",
7
+ "'" => "&#39;"
8
+ }
9
+
10
+ def html_escape
11
+ HTML_ESCAPE_CHARS.reduce(self){ |str, (replace_this, with_this)| str.gsub(replace_this, with_this) }
12
+ end
13
+
14
+ # TODO Fix up all of these
15
+ def sanitize
16
+ self.gsub('"','\"').gsub("<","&lt;").gsub(">", "&gt;")
17
+ end
18
+
19
+ def sanitize_quotes
20
+ self.gsub('"','&#34;')
21
+ end
22
+ end
@@ -0,0 +1,104 @@
1
+ # This class represents a "group" of Projects.
2
+ class Omniboard::Group
3
+ include Comparable
4
+
5
+ # The identifier for this group. Can be a string, object, whatever you like.
6
+ attr_accessor :identifier
7
+
8
+ # Deprecated methods
9
+ def name
10
+ $stderr.puts "Group#name is deprecated. Use Group#identifier instead."
11
+ @identifier
12
+ end
13
+
14
+ def name= n
15
+ $stderr.puts "Group#name= is deprecated. Use Group#identifier= instead."
16
+ @identifier = n
17
+ end
18
+
19
+ # The colour representation of a group
20
+ attr_accessor :colour
21
+
22
+ # Array of all groups in the board
23
+ @groups = []
24
+
25
+ class << self
26
+ # Global values for group colour brightness and saturation
27
+ attr_accessor :brightness, :saturation
28
+
29
+ # All groups in the board
30
+ attr_accessor :groups
31
+
32
+ # Find a group by identifier
33
+ def [] identifier
34
+ @groups.find{ |g| g.identifier == identifier } || new(identifier)
35
+ end
36
+
37
+ # Add a new group to the groups array. Also resets all colour assignments for the group.
38
+ # Note: usually called from initializer.
39
+ def add(g)
40
+ @groups.each(&:reset_colour)
41
+ @groups << g
42
+ end
43
+
44
+ # If we don't have any groups on our board, this is our default
45
+ # colour for ungrouped projects
46
+ def default_colour
47
+ Omniboard::Colour.new(0).standard
48
+ end
49
+ alias_method :colour, :default_colour
50
+
51
+ # If we don't have any groups on our board, this is our default
52
+ # light colour for ungrouped projects
53
+ def light_colour
54
+ Omniboard::Colour.new(0).light
55
+ end
56
+ end
57
+
58
+ def initialize(identifier)
59
+ raise(ArgumentError, "nil identifier not allowed") if identifier.nil?
60
+ @identifier = identifier
61
+ Omniboard::Group.add(self)
62
+ end
63
+
64
+ def colour
65
+ colour_obj.standard
66
+ end
67
+
68
+ def light_colour
69
+ colour_obj.light
70
+ end
71
+
72
+ def reset_colour
73
+ @colour_obj = nil
74
+ end
75
+
76
+ def assigned_colour
77
+ Omniboard::Column::colour_for_group(self.identifier)
78
+ end
79
+
80
+ def has_assigned_colour?
81
+ assigned_colour != nil
82
+ end
83
+
84
+ def to_s
85
+ @identifier
86
+ end
87
+
88
+ def <=> o
89
+ self.identifier <=> o.identifier
90
+ end
91
+
92
+ private
93
+ def colour_obj
94
+ @colour_obj ||= begin
95
+ my_hue = if has_assigned_colour?
96
+ assigned_colour
97
+ else
98
+ unassigned_groups = Omniboard::Group.groups.select{ |g| !g.has_assigned_colour? }
99
+ 360.0 / unassigned_groups.size * unassigned_groups.index(self)
100
+ end
101
+ Omniboard::Colour.new(my_hue)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,149 @@
1
+ module Omniboard
2
+ @configuration = {}
3
+
4
+ class << self
5
+ # Retrieve a configuration variable
6
+ def configuration(key)
7
+ @configuration[key]
8
+ end
9
+
10
+ # Set configuration variables from a dictionary
11
+ def configuration=(hsh)
12
+ @configuration = @configuration.merge(hsh)
13
+ end
14
+
15
+ # Set individual configuration variable
16
+ def configure(key,val)
17
+ @configuration[key] = val
18
+ end
19
+
20
+ #---------------------------------------
21
+ # Config location methods
22
+
23
+ # Set location where we'll store the configuration files
24
+ def config_location= loc
25
+ self.configure(:config_location, loc)
26
+ end
27
+
28
+ # Retrieve configuration_location
29
+ def config_location
30
+ @configuration[:config_location] || default_config_location
31
+ end
32
+
33
+ # Database usually exists here
34
+ def default_config_location
35
+ @default_config_location ||= File.join(ENV["HOME"], ".omniboard")
36
+ end
37
+
38
+ # Does the config folder exist?
39
+ def config_exists?
40
+ File.exists?(config_location)
41
+ end
42
+
43
+ # Populate with base classes
44
+ def populate
45
+
46
+ # 1. Make the columns folder
47
+ FileUtils::mkdir_p columns_location
48
+
49
+ # 2. Drop base columns + config into columns folder
50
+ Dir[File.join(__dir__, "columns/*.rb")].each{ |f| FileUtils::cp f, columns_location }
51
+
52
+ # 3. Add blank "custom.css" file
53
+ custom_css = File.join(config_location, "custom.css")
54
+ File.open(custom_css,"w"){ |io| io.puts "/* Custom CSS goes here */" }
55
+ end
56
+
57
+ #---------------------------------------
58
+ # Column-centric methods
59
+
60
+ # This is where we store columns
61
+ def columns_location
62
+ File.join(config_location, "columns")
63
+ end
64
+
65
+ # Fetch all columns!
66
+ def columns
67
+ @columns ||= begin
68
+ Dir[File.join(columns_location, "*")].each{ |f| require(f) } if File.exists?(columns_location)
69
+ Omniboard::Column.columns
70
+ end
71
+ end
72
+
73
+ #---------------------------------------
74
+ # Project-centric methods
75
+
76
+ def projects
77
+ @document.projects
78
+ end
79
+
80
+ #---------------------------------------
81
+ # Document-centric methods
82
+
83
+ # This is where our serialised document is located
84
+ def document_location
85
+ File.join(config_location, "db.yaml")
86
+ end
87
+
88
+ # Do we already have a serialised document?
89
+ def document_exists?
90
+ File.exists?(document_location)
91
+ end
92
+
93
+ # Load document from file
94
+ def load_document
95
+ @document = YAML::load_file(document_location)
96
+ end
97
+
98
+ # Save document to file
99
+ def save_document
100
+ FileUtils::mkdir_p config_location unless File.exists?(config_location)
101
+ File.open(document_location, "w"){ |io| io.puts YAML.dump(@document) }
102
+ end
103
+
104
+ # Update from an existing rubyfocus document
105
+ def update_document
106
+ @document.update
107
+ end
108
+
109
+ # Create a new document. For now, local documents are the only sort supported
110
+ def create_document
111
+ @document = Rubyfocus::Document.new(Rubyfocus::LocalFetcher.new)
112
+ end
113
+
114
+ # Arbitrarily assign a document object. Normally used only in debugging
115
+ attr_writer :document
116
+
117
+ #---------------------------------------
118
+ # Methods for reaching head
119
+ def document_at_head?
120
+ raise(RuntimeError, "Called Omniboard.document_at_head? before document loaded") if @document.nil?
121
+ return (
122
+ @document.fetcher.nil? || # Always at head if we have no fetcher
123
+ @document.fetcher.head == @document.patch_id
124
+ )
125
+ end
126
+
127
+ def document_can_reach_head?
128
+ raise(RuntimeError, "Called Omniboard.document_can_reach_head? before document loaded") if @document.nil?
129
+ return @document.fetcher.can_reach_head_from?(@document.patch_id)
130
+ end
131
+
132
+ def current_id; @document.patch_id; end
133
+ def head_id; @document.fetcher.head; end
134
+
135
+ #---------------------------------------
136
+ # Config-related methods
137
+
138
+ # Fetches any custom CSS stored in `custom.css` inside the config folder
139
+ def custom_css
140
+ custom_css_path = File.join(config_location, "custom.css")
141
+ if File.exists?(custom_css_path)
142
+ File.read(custom_css_path)
143
+ else
144
+ nil
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,119 @@
1
+ class Omniboard::ProjectWrapper
2
+ attr_accessor :project
3
+ attr_accessor :column
4
+ attr_accessor :group
5
+
6
+ # Is this project marked to show up specially?
7
+ attr_accessor :marked
8
+ alias_method :marked?, :marked
9
+
10
+ # Is this project dimmed (i.e. shown "faded out")
11
+ attr_accessor :dimmed
12
+ alias_method :dimmed?, :dimmed
13
+
14
+ # Should this project display an icon in the kanban? If so, what's the filename of the icon?
15
+ attr_accessor :icon
16
+
17
+ # If this project displayed an icon, we can supply an alternate text
18
+ attr_accessor :icon_alt
19
+
20
+ # Create a new project wrapper, wrapping project
21
+ def initialize(project, column: nil)
22
+ @project = project
23
+ @column = column
24
+ @marked = false
25
+ end
26
+
27
+ #---------------------------------------
28
+ # Colour methods
29
+ def colour
30
+ (@group || Omniboard::Group).colour
31
+ end
32
+
33
+ def light_colour
34
+ (@group || Omniboard::Group).light_colour
35
+ end
36
+
37
+ #---------------------------------------
38
+ # Number of tasks
39
+ def num_tasks
40
+ @num_tasks ||= project.incomplete_tasks.count
41
+ end
42
+
43
+ #---------------------------------------
44
+ # Runs on method missing. Allows project to step in and take
45
+ # the method if it can.
46
+ def method_missing(sym, *args, &blck)
47
+ if @project.respond_to?(sym)
48
+ @project.send(sym, *args, &blck)
49
+ else
50
+ raise NoMethodError, "undefined method #{sym} for #{self.inspect}"
51
+ end
52
+ end
53
+
54
+ #---------------------------------------
55
+ # Turn this project's tasks into a list
56
+ def task_list
57
+ tasks_to_list(@project.tasks)
58
+ end
59
+
60
+ # Format this project's note field to create nice html
61
+ def formatted_note
62
+ @formatted_note ||= Omniboard::StyledText.parse(self.note || "").to_html
63
+ end
64
+
65
+ def deferred_date
66
+ if self.start
67
+ self.start.strftime("%d %B %Y")
68
+ else
69
+ ""
70
+ end
71
+ end
72
+
73
+ def due_date
74
+ if self.due
75
+ self.due.strftime("%d %B %Y")
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ # A list of CSS classes to apply to this project
82
+ def css_classes
83
+ classes = ["project", column.display]
84
+ classes << "marked" if marked?
85
+ classes << "dimmed" if dimmed?
86
+ classes << "hidden" if dimmed? && column && column.property(:hide_dimmed)
87
+ classes
88
+ end
89
+
90
+ # Are all the available tasks in this project deferred?
91
+ def all_tasks_deferred?
92
+ next_tasks.size > 0 && actionable_tasks.size == 0
93
+ end
94
+
95
+ # How many days until one of our tasks becomes available?
96
+ def days_until_action
97
+ earliest_start = next_tasks.map(&:start).sort.first
98
+ if earliest_start.nil?
99
+ 0
100
+ else
101
+ earliest_start = earliest_start.to_date
102
+ if earliest_start < Date.today
103
+ 0
104
+ else
105
+ (earliest_start - Date.today).to_i
106
+ end
107
+ end
108
+ end
109
+
110
+ def to_s
111
+ self.project.to_s
112
+ end
113
+
114
+ private
115
+ def tasks_to_list(arr)
116
+ arr = arr.sort_by(&:rank)
117
+ "<ul>" + arr.map { |task| %|<li class="#{task.completed? ? "complete" : "incomplete"}">#{task.name}| + (task.has_subtasks? ? tasks_to_list(task.tasks) : "") + "</li>" }.join("") + "</ul>"
118
+ end
119
+ end
@@ -0,0 +1,53 @@
1
+ module Omniboard::Property
2
+ module PropertyClassMethods
3
+ def property p, allowed_values: nil, munge: nil
4
+ define_method(p) do |*args|
5
+ ivar = "@#{p}"
6
+ if args.empty?
7
+ instance_variable_get(ivar)
8
+ elsif args.size == 1
9
+ # Set values!
10
+ new_value = args.first
11
+ if allowed_values && !new_value.nil? && !allowed_values.include?(new_value)
12
+ raise(ArgumentError, "attempted to set property #{p} to forbidden value #{new_value.inspect}")
13
+ else
14
+ if new_value # Only run munge for non-nil values
15
+ case munge
16
+ when Symbol
17
+ new_value = new_value.send(munge)
18
+ when Proc
19
+ new_value = munge[new_value]
20
+ when nil
21
+ else
22
+ raise ArgumentError, "Property munge must be a symbol or a block - you supplied a #{munge.class}."
23
+ end
24
+ end
25
+ instance_variable_set(ivar, new_value)
26
+ end
27
+ else
28
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 0,1)"
29
+ end
30
+ end
31
+ end
32
+
33
+ # Block properties can also take non-block arguments
34
+ def block_property p
35
+ define_method(p) do |*args, &blck|
36
+ ivar = "@#{p}"
37
+ if blck
38
+ instance_variable_set(ivar, blck)
39
+ elsif args.size == 1
40
+ instance_variable_set(ivar, args.first)
41
+ elsif args.empty?
42
+ instance_variable_get(ivar)
43
+ else
44
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 0,1)"
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.included mod
51
+ mod.extend PropertyClassMethods
52
+ end
53
+ end