ursm-ditz 0.4

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/lib/html.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'erb'
2
+
3
+ module Ditz
4
+
5
+ ## pass through any variables needed for template generation, and add a bunch
6
+ ## of HTML formatting utility methods.
7
+ class ErbHtml
8
+ def initialize template_dir, links, binding={}
9
+ @template_dir = template_dir
10
+ @links = links
11
+ @binding = binding
12
+ end
13
+
14
+ ## return an ErbHtml object that has the current binding plus extra_binding merged in
15
+ def clone_for_binding extra_binding={}
16
+ extra_binding.empty? ? self : ErbHtml.new(@template_dir, @links, @binding.merge(extra_binding))
17
+ end
18
+
19
+ def render_template template_name, extra_binding={}
20
+ if extra_binding.empty?
21
+ @@erbs ||= {}
22
+ @@erbs[template_name] ||= ERB.new IO.read(File.join(@template_dir, "#{template_name}.rhtml"))
23
+ @@erbs[template_name].result binding
24
+ else
25
+ clone_for_binding(extra_binding).render_template template_name
26
+ end
27
+ end
28
+
29
+ def render_string s, extra_binding={}
30
+ if extra_binding.empty?
31
+ ERB.new(s).result binding
32
+ else
33
+ clone_for_binding(extra_binding).render_string s
34
+ end
35
+ end
36
+
37
+ ###
38
+ ### the following methods are meant to be called from the ERB itself
39
+ ###
40
+
41
+ def h o; o.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
42
+ def t o; o.strftime "%Y-%m-%d %H:%M %Z" end
43
+ def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
44
+ def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
45
+ def link_to o, name
46
+ dest = @links[o]
47
+ dest = o if dest.nil? && o.is_a?(String)
48
+ raise ArgumentError, "no link for #{o.inspect}" unless dest
49
+ "<a href=\"#{dest}\">#{name}</a>"
50
+ end
51
+ def fancy_issue_link_for i
52
+ "<span class=\"issuestatus_#{i.status}\">" + link_to(i, "[#{i.title}]") + "</span>"
53
+ end
54
+
55
+ def link_issue_names project, s
56
+ project.issues.inject(s) do |s, i|
57
+ s.gsub(/\b#{i.name}\b/, fancy_issue_link_for(i))
58
+ end
59
+ end
60
+
61
+ ## render a nested ERB
62
+ alias :render :render_template
63
+
64
+ def method_missing meth, *a
65
+ @binding.member?(meth) ? @binding[meth] : super
66
+ end
67
+ end
68
+
69
+ end
data/lib/index.rhtml ADDED
@@ -0,0 +1,113 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
+ <head>
6
+ <title><%= project.name %> Issue Tracker</title>
7
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
8
+ <link rel="stylesheet" href="style.css" type="text/css" />
9
+ </head>
10
+ <body>
11
+
12
+ <h1><%= project.name %> Issue Tracker</h1>
13
+
14
+ <h2>Upcoming Releases</h2>
15
+ <% if upcoming_releases.empty? %>
16
+ <p>No upcoming releases.</p>
17
+ <% else %>
18
+ <ul>
19
+ <% upcoming_releases.each do |r| %>
20
+ <%
21
+ issues = project.issues_for_release r
22
+ num_done = issues.count_of { |i| i.closed? }
23
+ pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
24
+ open_issues = project.group_issues(issues.select { |i| i.open? })
25
+ %>
26
+ <li>
27
+ <%= link_to r, "Release #{r.name}" %>:
28
+ <% if issues.empty? %>
29
+ no issues
30
+ <% else %>
31
+ <%= sprintf "%.0f%%", pct_done %> complete;
32
+ <% if open_issues.empty? %>
33
+ ready for release!
34
+ <% else %>
35
+ <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
36
+ <% end %>
37
+ <% end %>
38
+ </li>
39
+ <% end %>
40
+ </ul>
41
+ <% end %>
42
+
43
+ <h2>Past Releases</h2>
44
+ <% if past_releases.empty? %>
45
+ <p>No past releases.</p>
46
+ <% else %>
47
+ <ul>
48
+ <% past_releases.sort_by { |r| r.release_time }.reverse.each do |r| %>
49
+ <li><%= link_to r, "Release #{r.name}" %>, released <%= r.release_time.pretty_date %>. </li>
50
+ <% end %>
51
+ </ul>
52
+ <% end %>
53
+
54
+ <h2>Unassigned issues</h2>
55
+ <%
56
+ issues = project.unassigned_issues
57
+ open_issues = issues.select { |i| i.open? }
58
+ %>
59
+ <p>
60
+ <% if issues.empty? %>
61
+ No unassigned issues.
62
+ <% else %>
63
+ <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).capitalize %>; <%= open_issues.size.to_pretty_s %> of them open.
64
+ <% end %>
65
+ </p>
66
+
67
+ <% if components.size > 1 %>
68
+ <h2>Open Issues by component</h2>
69
+ <ul>
70
+ <% components.each do |c| %>
71
+ <%
72
+ open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
73
+ %>
74
+ <li>
75
+ <% if open_issues.empty? %>
76
+ <span class="dimmed">
77
+ <%= link_to c, c.name %>:
78
+ no open issues.
79
+ </span>
80
+ <% else %>
81
+ <%= link_to c, c.name %>:
82
+ <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
83
+ <% end %>
84
+ </li>
85
+ <% end %>
86
+ </ul>
87
+ <% end %>
88
+
89
+ <h2>Recent activity</h2>
90
+
91
+ <table>
92
+ <% project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
93
+ flatten_one_level.
94
+ sort_by { |e| e.first.first }.
95
+ reverse[0 ... 10].
96
+ each do |(date, who, what, comment), i| %>
97
+ <tr>
98
+ <td><%= date.pretty_date %></td>
99
+ <td class="issuename">
100
+ <%= link_issue_names project, h(i.name) %>
101
+ <%= what %> by <%= who.shortened_email %></td>
102
+ </tr>
103
+ <% if comment && comment =~ /\S/ %>
104
+ <tr><td></td><td><i><%= link_issue_names project, h(comment) %></i></td></tr>
105
+ <% end %>
106
+ <% end %>
107
+ </table>
108
+
109
+ <p class="footer">Generated by <a
110
+ href="http://ditz.rubyforge.org/">ditz</a>.</p>
111
+
112
+ </body>
113
+ </html>
data/lib/issue.rhtml ADDED
@@ -0,0 +1,111 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
+ <head>
6
+ <title><%= issue.title %></title>
7
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
8
+ <link rel="stylesheet" href="style.css" type="text/css" />
9
+ </head>
10
+
11
+ <body>
12
+
13
+ <div><%= link_to "index", "&laquo; #{project.name} project page" %></div>
14
+
15
+ <h1><%= link_issue_names project, issue.title %></h1>
16
+
17
+ <%= link_issue_names project, p(issue.desc) %>
18
+
19
+ <table>
20
+ <tr>
21
+ <td class="attrname">Id:</td>
22
+ <td class="attrval"><%= issue.id %></td>
23
+ </tr>
24
+
25
+ <tr>
26
+ <td class="attrname">Type:</td>
27
+ <td class="attrval"><%= issue.type %></td>
28
+ </tr>
29
+
30
+ <tr>
31
+ <td class="attrname">Creation time:</td>
32
+ <td class="attrval"><%= issue.creation_time %></td>
33
+ </tr>
34
+
35
+ <tr>
36
+ <td class="attrname">Creator:</td>
37
+ <td class="attrval"><%=obscured_email issue.reporter %></td>
38
+ </tr>
39
+
40
+ <% unless issue.references.empty? %>
41
+ <tr>
42
+ <td class="attrname">References:</td>
43
+ <td class="attrval">
44
+ <% issue.references.each_with_index do |r, i| %>
45
+ [<%= i + 1 %>] <%= link_to r, r %><br/>
46
+ <% end %>
47
+ </td>
48
+ </tr>
49
+
50
+ <% end %>
51
+
52
+ <tr>
53
+ <td class="attrname">Release:</td>
54
+ <td class="attrval">
55
+ <% if release %>
56
+ <%= link_to release, release.name %>
57
+ <% if release.released? %>
58
+ (released <%= release.release_time.pretty_date %>)
59
+ <% else %>
60
+ (unreleased)
61
+ <% end %>
62
+ <% else %>
63
+ <%= link_to "unassigned", "unassigned" %>
64
+ <% end %>
65
+ </td>
66
+ </tr>
67
+
68
+ <tr>
69
+ <td class="attrname">Component:</td>
70
+ <td class="attrval"><%= link_to component, component.name %></td>
71
+ </tr>
72
+
73
+ <tr>
74
+ <td class="attrname">Status:</td>
75
+ <td class="attrval">
76
+ <%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
77
+ </td>
78
+ </tr>
79
+
80
+ <%= extra_summary_html %>
81
+
82
+ </table>
83
+
84
+ <%= extra_details_html %>
85
+
86
+ <h2>Issue log</h2>
87
+
88
+ <table>
89
+ <% issue.log_events.each_with_index do |(time, who, what, comment), i| %>
90
+ <% if i % 2 == 0 %>
91
+ <tr class="logentryeven">
92
+ <% else %>
93
+ <tr class="logentryodd">
94
+ <% end %>
95
+ <td class="logtime"><%=t time %></td>
96
+ <td class="logwho"><%=obscured_email who %></td>
97
+ <td class="logwhat"><%=h what %></td>
98
+ </tr>
99
+ <tr><td colspan="3" class="logcomment">
100
+ <% if comment.empty? %>
101
+ <% else %>
102
+ <%= link_issue_names project, p(comment) %>
103
+ <% end %>
104
+ </td></tr>
105
+ <% end %>
106
+ </table>
107
+
108
+ <p class="footer">Generated by <a
109
+ href="http://ditz.rubyforge.org/">ditz</a>.</p>
110
+ </body>
111
+ </html>
@@ -0,0 +1,33 @@
1
+ <table>
2
+ <% issues.sort_by { |i| i.sort_order }.each do |i| %>
3
+ <tr>
4
+ <td class="issuestatus_<%= i.status %>">
5
+ <% if i.closed? %>
6
+ <%= i.disposition_string %>
7
+ <% else %>
8
+ <%= i.status_string %>
9
+ <% end %>
10
+ </td>
11
+ <td class="issuename">
12
+ <%= fancy_issue_link_for i %>
13
+ <%= i.bug? ? '(bug)' : '' %>
14
+ </td>
15
+ <% if show_release %>
16
+ <td class="issuerelease">
17
+ <% if i.release %>
18
+ <% r = project.release_for i.release %>
19
+ in <%= link_to r, "release #{i.release}" %>
20
+ (<%= r.status %>)
21
+ <% else %>
22
+ <% end %>
23
+ </td>
24
+ <% end %>
25
+ <% if show_component %>
26
+ <td class="issuecomponent">
27
+ component <%= link_to project.component_for(i.component), i.component %>
28
+ </td>
29
+ <% end %>
30
+ </tr>
31
+ <% end %>
32
+ </table>
33
+
data/lib/lowline.rb ADDED
@@ -0,0 +1,202 @@
1
+ require 'tempfile'
2
+ require "util"
3
+
4
+ class Numeric
5
+ def to_pretty_s
6
+ %w(zero one two three four five six seven eight nine ten)[self] || to_s
7
+ end
8
+ end
9
+
10
+ class String
11
+ def dcfirst; self[0..0].downcase + self[1..-1] end
12
+ def blank?; self =~ /\A\s*\z/ end
13
+ def underline; self + "\n" + ("-" * self.length) end
14
+ def pluralize n, b=true
15
+ s = (n == 1 ? self : (self == 'bugfix' ? 'bugfixes' : self + "s")) # oh yeah
16
+ b ? n.to_pretty_s + " " + s : s
17
+ end
18
+ def shortened_email; self =~ /<?(\S+?)@.+/ ? $1 : self end
19
+ def multistrip; strip.gsub(/\n\n+/, "\n\n") end
20
+ end
21
+
22
+ class Array
23
+ def listify prefix=""
24
+ return "" if empty?
25
+ "\n" +
26
+ map_with_index { |x, i| x.to_s.gsub(/^/, "#{prefix}#{i + 1}. ") }.
27
+ join("\n")
28
+ end
29
+ end
30
+
31
+ class Time
32
+ def pretty; strftime "%c" end
33
+ def pretty_date; strftime "%Y-%m-%d" end
34
+ def ago
35
+ diff = (Time.now - self).to_i.abs
36
+ if diff < 60
37
+ "second".pluralize diff
38
+ elsif diff < 60*60*3
39
+ "minute".pluralize(diff / 60)
40
+ elsif diff < 60*60*24*3
41
+ "hour".pluralize(diff / (60*60))
42
+ elsif diff < 60*60*24*7*2
43
+ "day".pluralize(diff / (60*60*24))
44
+ elsif diff < 60*60*24*7*8
45
+ "week".pluralize(diff / (60*60*24*7))
46
+ elsif diff < 60*60*24*7*52
47
+ "month".pluralize(diff / (60*60*24*7*4))
48
+ else
49
+ "year".pluralize(diff / (60*60*24*7*52))
50
+ end
51
+ end
52
+ end
53
+
54
+ module Lowline
55
+ def run_editor
56
+ f = Tempfile.new "ditz"
57
+ yield f
58
+ f.close
59
+
60
+ editor = ENV["EDITOR"] || "/usr/bin/vi"
61
+ cmd = "#{editor} #{f.path.inspect}"
62
+
63
+ mtime = File.mtime f.path
64
+ system cmd or raise Error, "cannot execute command: #{cmd.inspect}"
65
+
66
+ File.mtime(f.path) == mtime ? nil : f.path
67
+ end
68
+
69
+ def ask q, opts={}
70
+ default_s = case opts[:default]
71
+ when nil; nil
72
+ when ""; " (enter for none)"
73
+ else; " (enter for #{opts[:default].inspect})"
74
+ end
75
+
76
+ tail = case q
77
+ when /[:?]$/; " "
78
+ when /[:?]\s+$/; ""
79
+ else; ": "
80
+ end
81
+
82
+ while true
83
+ prompt = [q, default_s, tail].compact.join
84
+ if Ditz::has_readline?
85
+ ans = Readline::readline(prompt)
86
+ else
87
+ print prompt
88
+ ans = STDIN.gets.strip
89
+ end
90
+ if opts[:default]
91
+ ans = opts[:default] if ans.blank?
92
+ else
93
+ next if ans.blank? && !opts[:empty_ok]
94
+ end
95
+ break ans unless (opts[:restrict] && ans !~ opts[:restrict])
96
+ end
97
+ end
98
+
99
+ def ask_via_editor q, default=nil
100
+ fn = run_editor do |f|
101
+ f.puts q.gsub(/^/, "## ")
102
+ f.puts "##"
103
+ f.puts "## Enter your text below. Lines starting with a '#' will be ignored."
104
+ f.puts
105
+ f.puts default if default
106
+ end
107
+ return unless fn
108
+ IO.read(fn).gsub(/^#.*$/, "").multistrip
109
+ end
110
+
111
+ def ask_multiline q
112
+ puts "#{q} (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):"
113
+ ans = ""
114
+ while true
115
+ if Ditz::has_readline?
116
+ line = Readline::readline('> ')
117
+ else
118
+ (line = STDIN.gets) && line.strip!
119
+ end
120
+ if line
121
+ if Ditz::has_readline?
122
+ Readline::HISTORY.push(line)
123
+ end
124
+ case line
125
+ when /^\.$/, "/stop"
126
+ break
127
+ when "/reset"
128
+ return ask_multiline(q)
129
+ when "/edit"
130
+ return ask_via_editor(q, ans)
131
+ else
132
+ ans << line + "\n"
133
+ end
134
+ else
135
+ puts
136
+ break
137
+ end
138
+ end
139
+ ans.multistrip
140
+ end
141
+
142
+ def ask_yon q
143
+ while true
144
+ print "#{q} (y/n): "
145
+ a = STDIN.gets.strip
146
+ break a if a =~ /^[yn]$/i
147
+ end =~ /y/i
148
+ end
149
+
150
+ def ask_for_many plural_name, name=nil
151
+ name ||= plural_name.gsub(/s$/, "")
152
+ stuff = []
153
+
154
+ while true
155
+ puts
156
+ puts "Current #{plural_name}:"
157
+ if stuff.empty?
158
+ puts "None!"
159
+ else
160
+ stuff.each_with_index { |c, i| puts " #{i + 1}) #{c}" }
161
+ end
162
+ puts
163
+ ans = ask "(A)dd #{name}, (r)emove #{name}, or (d)one"
164
+ case ans
165
+ when "a", "A"
166
+ ans = ask "#{name.capitalize} name", ""
167
+ stuff << ans unless ans =~ /^\s*$/
168
+ when "r", "R"
169
+ ans = ask "Remove which #{name}? (1--#{stuff.size})"
170
+ stuff.delete_at(ans.to_i - 1) if ans
171
+ when "d", "D"
172
+ break
173
+ end
174
+ end
175
+ stuff
176
+ end
177
+
178
+ def ask_for_selection stuff, name, to_string=:to_s
179
+ puts "Choose a #{name}:"
180
+ stuff.each_with_index do |c, i|
181
+ pretty = case to_string
182
+ when block_given? && to_string # heh
183
+ yield c
184
+ when Symbol
185
+ c.send to_string
186
+ when Proc
187
+ to_string.call c
188
+ else
189
+ raise ArgumentError, "unknown to_string argument type; expecting Proc or Symbol"
190
+ end
191
+ puts " #{i + 1}) #{pretty}"
192
+ end
193
+
194
+ j = while true
195
+ i = ask "#{name.capitalize} (1--#{stuff.size})"
196
+ break i.to_i if i && (1 .. stuff.size).member?(i.to_i)
197
+ end
198
+
199
+ stuff[j - 1]
200
+ end
201
+ end
202
+