ursm-ditz 0.4

Sign up to get free protection for your applications and to get access to all the features.
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
+