ditz 0.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.
- data/Changelog +2 -0
- data/README.txt +117 -0
- data/Rakefile +48 -0
- data/bin/ditz +77 -0
- data/lib/component.rhtml +18 -0
- data/lib/ditz.rb +13 -0
- data/lib/html.rb +41 -0
- data/lib/index.rhtml +83 -0
- data/lib/issue.rhtml +106 -0
- data/lib/issue_table.rhtml +29 -0
- data/lib/lowline.rb +150 -0
- data/lib/model-objects.rb +265 -0
- data/lib/model.rb +137 -0
- data/lib/operator.rb +408 -0
- data/lib/release.rhtml +67 -0
- data/lib/style.css +91 -0
- data/lib/unassigned.rhtml +27 -0
- data/lib/util.rb +33 -0
- metadata +79 -0
data/Changelog
ADDED
data/README.txt
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
== ditz
|
2
|
+
|
3
|
+
by William Morgan <wmorgan-ditz@masanjin.net>
|
4
|
+
|
5
|
+
http://ditz.rubyforge.org
|
6
|
+
|
7
|
+
== DESCRIPTION
|
8
|
+
|
9
|
+
Ditz is a simple, light-weight distributed issue tracker designed to work with
|
10
|
+
distributed version control systems like darcs and git. Ditz maintains an issue
|
11
|
+
database file on disk, written in a line-based and human-editable format. This
|
12
|
+
file can be kept under version control alongside project code, and issue state
|
13
|
+
change can then be handled like code changes: modified as part of a commit,
|
14
|
+
merged with other state changes from other developers, conflict-resolved in the
|
15
|
+
standard manner, etc.
|
16
|
+
|
17
|
+
Ditz provides a simple, console-based interface for creating and updating the
|
18
|
+
issue database file, and some rudimentary HTML generation capabilities for
|
19
|
+
producing world-readable status pages. It offers no central public method of
|
20
|
+
bug submission.
|
21
|
+
|
22
|
+
== SYNOPSIS
|
23
|
+
|
24
|
+
# set up project. creates the bugs.yaml file.
|
25
|
+
1. ditz init
|
26
|
+
2. ditz add-release
|
27
|
+
|
28
|
+
# add an issue
|
29
|
+
3. ditz add
|
30
|
+
|
31
|
+
# where am i?
|
32
|
+
4. ditz status
|
33
|
+
5. ditz todo
|
34
|
+
|
35
|
+
# do work
|
36
|
+
6. write code
|
37
|
+
7. ditz close <issue-id>
|
38
|
+
8. commit
|
39
|
+
9. goto 3
|
40
|
+
|
41
|
+
# finished!
|
42
|
+
10. ditz release <release-name>
|
43
|
+
|
44
|
+
== THE DITZ DATA MODEL
|
45
|
+
|
46
|
+
Ditz includes the bare minimum set of features necessary for open-source
|
47
|
+
development. Features like time spent, priority, assignment of tasks to
|
48
|
+
developers, due dates, etc. are purposely excluded.
|
49
|
+
|
50
|
+
A ditz project consists of issues, releases and components.
|
51
|
+
|
52
|
+
Issues:
|
53
|
+
Issues are the fundamental currency of issue tracking. A ditz issue is either
|
54
|
+
a feature or a bug, but this distinction doesn't affect anything other than
|
55
|
+
how they're displayed.
|
56
|
+
|
57
|
+
Each issue belongs to exactly one component, and is part of zero or one
|
58
|
+
releases.
|
59
|
+
|
60
|
+
Each issues has an exportable id, in the form of 40 random hex characters.
|
61
|
+
This id is "guaranteed" to be unique across all possible issues and
|
62
|
+
developers, present and future. Issue ids are typically not exposed to the
|
63
|
+
user.
|
64
|
+
|
65
|
+
Issues also have a non-exportable name, which is short and human-readable.
|
66
|
+
All ditz commands use issue names instead of issue ids. Issue ids may change
|
67
|
+
in certain circumstances, specifically after a "ditz drop" command.
|
68
|
+
|
69
|
+
Components:
|
70
|
+
There is always one "general" component, named after the project itself. In
|
71
|
+
the simplest case, this is the only component, and the user is never bothered
|
72
|
+
with the question of which component to assign an issue to.
|
73
|
+
|
74
|
+
Components simply provide a way of organizing issues, and have no real
|
75
|
+
functionality. Issues are assigned names derived form the component they're
|
76
|
+
assigned to.
|
77
|
+
|
78
|
+
Releases:
|
79
|
+
A release is the primary grouping mechanism for issues. Status commands like
|
80
|
+
"ditz status" and "ditz todo" always group issues by release. When a release
|
81
|
+
is 100% complete, it can be marked as released, in which case the associated
|
82
|
+
issues will cease appearing in ditz status and todo messages.
|
83
|
+
|
84
|
+
== FUTURE WORK
|
85
|
+
|
86
|
+
In future releases, Ditz will have a plugin architecture to allow tighter
|
87
|
+
integration with specific SCMs and developer communication channels. (See
|
88
|
+
http://ditz.rubyforge.org/ditz/issue-0704dafe4aef96279364013aba177a0971d425cb.html)
|
89
|
+
|
90
|
+
== LEARNING MORE
|
91
|
+
|
92
|
+
* ditz help
|
93
|
+
* find $DITZ_INSTALL_DIR -type f | xargs cat
|
94
|
+
|
95
|
+
== REQUIREMENTS
|
96
|
+
|
97
|
+
* trollop >= 1.7
|
98
|
+
|
99
|
+
== INSTALLATION
|
100
|
+
|
101
|
+
Download tarballs from http://rubyforge.org/projects/ditz/, or command your
|
102
|
+
computer to "gem install ditz".
|
103
|
+
|
104
|
+
== LICENSE
|
105
|
+
|
106
|
+
Copyright (c) 2008 William Morgan.
|
107
|
+
|
108
|
+
This program is free software: you can redistribute it and/or modify
|
109
|
+
it under the terms of the GNU General Public License as published by
|
110
|
+
the Free Software Foundation, either version 3 of the License, or
|
111
|
+
(at your option) any later version.
|
112
|
+
|
113
|
+
This program is distributed in the hope that it will be useful,
|
114
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
115
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
116
|
+
GNU General Public License for more details.
|
117
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
|
4
|
+
$:.unshift "lib"
|
5
|
+
require 'ditz'
|
6
|
+
|
7
|
+
class Hoe
|
8
|
+
def extra_deps; @extra_deps.reject { |x| Array(x).first == "hoe" } end
|
9
|
+
end # thanks to "Mike H"
|
10
|
+
|
11
|
+
Hoe.new('ditz', Ditz::VERSION) do |p|
|
12
|
+
p.rubyforge_name = 'ditz'
|
13
|
+
p.author = "William Morgan"
|
14
|
+
p.summary = "A simple issue tracker designed to integrate well with distributed version control systems like git and darcs. State is saved to a YAML file kept under version control, allowing issues to be closed/added/modified as part of a commit."
|
15
|
+
|
16
|
+
p.description = p.paragraphs_of('README.txt', 4..5, 9..18).join("\n\n").gsub(/== SYNOPSIS/, "Synopsis")
|
17
|
+
p.url = "http://ditz.rubyforge.org"
|
18
|
+
p.changes = p.paragraphs_of('Changelog', 0..0).join("\n\n")
|
19
|
+
p.email = "wmorgan-ditz@masanjin.net"
|
20
|
+
p.extra_deps = [['trollop', '>= 1.7']]
|
21
|
+
end
|
22
|
+
|
23
|
+
WWW_FILES = FileList["www/*"] + %w(README.txt)
|
24
|
+
SCREENSHOTS = FileList["www/ss?.png"]
|
25
|
+
SCREENSHOTS_SMALL = []
|
26
|
+
SCREENSHOTS.each do |fn|
|
27
|
+
fn =~ /ss(\d+)\.png/
|
28
|
+
sfn = "www/ss#{$1}-small.png"
|
29
|
+
file sfn => [fn] do |t|
|
30
|
+
sh "cat #{fn} | pngtopnm | pnmscale -xysize 320 240 | pnmtopng > #{sfn}"
|
31
|
+
end
|
32
|
+
SCREENSHOTS_SMALL << sfn
|
33
|
+
end
|
34
|
+
|
35
|
+
task :upload_webpage => WWW_FILES do |t|
|
36
|
+
sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
|
37
|
+
end
|
38
|
+
|
39
|
+
task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
|
40
|
+
sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
|
41
|
+
end
|
42
|
+
|
43
|
+
task :upload_report do |t|
|
44
|
+
sh "ditz html ditz"
|
45
|
+
sh "scp -Cr ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
|
46
|
+
end
|
47
|
+
|
48
|
+
# vim: syntax=ruby
|
data/bin/ditz
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'; include Trollop
|
5
|
+
require "ditz"
|
6
|
+
|
7
|
+
$opts = options do
|
8
|
+
version "ditz #{Ditz::VERSION} (c) 2008 William Morgan"
|
9
|
+
opt :issue_file, "Issue database file", :default => "bugs.yaml"
|
10
|
+
opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], ".ditz-config")
|
11
|
+
opt :verbose, "Verbose output", :default => false
|
12
|
+
end
|
13
|
+
|
14
|
+
cmd = ARGV.shift or die "expecting a ditz command"
|
15
|
+
op = Ditz::Operator.new
|
16
|
+
|
17
|
+
case cmd # special cases: init and help
|
18
|
+
when "init"
|
19
|
+
fn = $opts[:issue_file]
|
20
|
+
die "#{fn} already exists" if File.exists? fn
|
21
|
+
project = op.init
|
22
|
+
project.save! fn
|
23
|
+
puts "Ok, #{fn} created successfully."
|
24
|
+
exit
|
25
|
+
when "help"
|
26
|
+
op.help
|
27
|
+
exit
|
28
|
+
end
|
29
|
+
|
30
|
+
Ditz::debug "loading issues from #{$opts[:issue_file]}"
|
31
|
+
project = begin
|
32
|
+
Ditz::Project.from $opts[:issue_file]
|
33
|
+
rescue SystemCallError, Ditz::Project::Error => e
|
34
|
+
die "#{e.message} (use 'init' to initialize)"
|
35
|
+
end
|
36
|
+
|
37
|
+
project.validate!
|
38
|
+
project.assign_issue_names!
|
39
|
+
project.each_modelobject { |o| o.after_deserialize project }
|
40
|
+
|
41
|
+
config = begin
|
42
|
+
fn = ".ditz-config"
|
43
|
+
if File.exists? fn
|
44
|
+
Ditz::debug "loading config from #{fn}"
|
45
|
+
Ditz::Config.from fn
|
46
|
+
else
|
47
|
+
Ditz::debug "loading config from #{$opts[:config_file]}"
|
48
|
+
Ditz::Config.from $opts[:config_file]
|
49
|
+
end
|
50
|
+
rescue SystemCallError, Ditz::ModelObject::ModelError => e
|
51
|
+
puts <<EOS
|
52
|
+
I wasn't able to find a configuration file #{$opts[:config_file]}.
|
53
|
+
We'll set it up right now.
|
54
|
+
EOS
|
55
|
+
Ditz::Config.create_interactively
|
56
|
+
end
|
57
|
+
|
58
|
+
unless op.has_operation? cmd
|
59
|
+
die "no such command: #{cmd}"
|
60
|
+
end
|
61
|
+
|
62
|
+
## talk about the law of unintended consequences. 'gets' requires this.
|
63
|
+
args = []
|
64
|
+
args << ARGV.shift until ARGV.empty?
|
65
|
+
|
66
|
+
Ditz::debug "executing command #{cmd}"
|
67
|
+
op.do cmd, project, config, *args
|
68
|
+
|
69
|
+
dirty = project.each_modelobject { |o| break true if o.changed? } || false
|
70
|
+
if dirty
|
71
|
+
Ditz::debug "project is dirty, saving"
|
72
|
+
project.each_modelobject { |o| o.before_serialize project }
|
73
|
+
project.save! $opts[:issue_file]
|
74
|
+
end
|
75
|
+
config.save! $opts[:config_file] if config.changed?
|
76
|
+
|
77
|
+
# vim: syntax=ruby
|
data/lib/component.rhtml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
2
|
+
<head>
|
3
|
+
<title>Component <%= component.name %></title>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
5
|
+
<link rel="stylesheet" href="style.css" type="text/css" />
|
6
|
+
</head>
|
7
|
+
|
8
|
+
<body>
|
9
|
+
|
10
|
+
<%= link_to "index", "« #{project.name} project page" %>
|
11
|
+
|
12
|
+
<h1><%= project.name %> component: <%= component.name %></h1>
|
13
|
+
|
14
|
+
<%= render "issue_table", :show_component => false, :show_release => true %>
|
15
|
+
|
16
|
+
<p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
|
17
|
+
</body>
|
18
|
+
</html>
|
data/lib/ditz.rb
ADDED
data/lib/html.rb
ADDED
@@ -0,0 +1,41 @@
|
|
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, template_name, links, mapping={}
|
9
|
+
@template_name = template_name
|
10
|
+
@template_dir = template_dir
|
11
|
+
@links = links
|
12
|
+
@mapping = mapping
|
13
|
+
|
14
|
+
@@erbs ||= {}
|
15
|
+
@@erbs[template_name] ||= ERB.new(IO.readlines(File.join(template_dir, "#{template_name}.rhtml")).join)
|
16
|
+
end
|
17
|
+
|
18
|
+
def h o; o.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") end
|
19
|
+
def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
|
20
|
+
def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
|
21
|
+
def link_to o, name
|
22
|
+
dest = @links[o]
|
23
|
+
dest = o if dest.nil? && o.is_a?(String)
|
24
|
+
raise ArgumentError, "no link for #{o.inspect}" unless dest
|
25
|
+
"<a href=\"#{dest}\">#{name}</a>"
|
26
|
+
end
|
27
|
+
|
28
|
+
def render template_name, morevars={}
|
29
|
+
ErbHtml.new(@template_dir, template_name, @links, @mapping.merge(morevars)).to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing meth, *a
|
33
|
+
@mapping.member?(meth) ? @mapping[meth] : super
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
@@erbs[@template_name].result binding
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/lib/index.rhtml
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
2
|
+
<head>
|
3
|
+
<title><%= project.name %> Issue Tracker</title>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
5
|
+
<link rel="stylesheet" href="style.css" type="text/css" />
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
|
9
|
+
<h1><%= project.name %> Issue Tracker</h1>
|
10
|
+
|
11
|
+
<h2>Upcoming Releases</h2>
|
12
|
+
<% if upcoming_releases.empty? %>
|
13
|
+
<p>No upcoming releases.</p>
|
14
|
+
<% else %>
|
15
|
+
<ul>
|
16
|
+
<% upcoming_releases.each do |r| %>
|
17
|
+
<%
|
18
|
+
issues = project.issues_for_release r
|
19
|
+
num_done = issues.count_of { |i| i.closed? }
|
20
|
+
pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
|
21
|
+
num_bugs_todo = issues.count_of { |i| i.bug? && i.open? }
|
22
|
+
num_feats_todo = issues.count_of { |i| i.feature? && i.open? }
|
23
|
+
%>
|
24
|
+
<li>
|
25
|
+
<%= link_to r, "Release #{r.name}" %>:
|
26
|
+
<% if issues.empty? %>
|
27
|
+
no issues
|
28
|
+
<% else %>
|
29
|
+
<%= sprintf "%.0f%%", pct_done %> complete;
|
30
|
+
<%= "bug".pluralize num_bugs_todo %> and
|
31
|
+
<%= "feature".pluralize num_feats_todo %> open.
|
32
|
+
<% end %>
|
33
|
+
</li>
|
34
|
+
<% end %>
|
35
|
+
</ul>
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
<h2>Past Releases</h2>
|
39
|
+
<% if past_releases.empty? %>
|
40
|
+
<p>No past releases.</p>
|
41
|
+
<% else %>
|
42
|
+
<ul>
|
43
|
+
<% past_releases.each do |r| %>
|
44
|
+
<li><%= link_to r, "Release #{r.name}" %>, released <%= r.release_time.pretty_date %>. </li>
|
45
|
+
<% end %>
|
46
|
+
</ul>
|
47
|
+
<% end %>
|
48
|
+
|
49
|
+
<h2>Unassigned issues</h2>
|
50
|
+
<%
|
51
|
+
issues = project.unassigned_issues
|
52
|
+
open_issues = issues.select { |i| i.open? }
|
53
|
+
%>
|
54
|
+
<p>
|
55
|
+
<%= link_to "unassigned", "unassigned issue".pluralize(issues.size).ucfirst %>; <%= open_issues.size.to_pretty_s %> open.
|
56
|
+
</p>
|
57
|
+
|
58
|
+
<% if components.size > 1 %>
|
59
|
+
<h2>Open Issues by component</h2>
|
60
|
+
<ul>
|
61
|
+
<% components.each do |c| %>
|
62
|
+
<%
|
63
|
+
open_issues = project.issues_for_component(c).select { |i| i.open? }
|
64
|
+
num_bugs_todo = open_issues.count_of { |i| i.bug? }
|
65
|
+
num_feats_todo = open_issues.count_of { |i| i.feature? }
|
66
|
+
%>
|
67
|
+
<li>
|
68
|
+
<%= link_to c, c.name %>:
|
69
|
+
<% if open_issues.empty? %>
|
70
|
+
no open issues.
|
71
|
+
<% else %>
|
72
|
+
<%= "open bug".pluralize num_bugs_todo %> and
|
73
|
+
<%= "open feature".pluralize num_feats_todo %>.
|
74
|
+
<% end %>
|
75
|
+
</li>
|
76
|
+
<% end %>
|
77
|
+
</ul>
|
78
|
+
<% end %>
|
79
|
+
|
80
|
+
<p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
|
81
|
+
|
82
|
+
</body>
|
83
|
+
</html>
|
data/lib/issue.rhtml
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
2
|
+
<head>
|
3
|
+
<title><%= issue.title %></title>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
5
|
+
<link rel="stylesheet" href="style.css" type="text/css" />
|
6
|
+
</head>
|
7
|
+
|
8
|
+
<body>
|
9
|
+
|
10
|
+
<%= link_to "index", "« #{project.name} project page" %>
|
11
|
+
|
12
|
+
<h1><%= issue.title %></h1>
|
13
|
+
|
14
|
+
<%=
|
15
|
+
project.issues.inject(p(issue.desc)) do |s, i|
|
16
|
+
s.gsub(/\{issue #{i.id}\}/, link_to(i, i.title))
|
17
|
+
end.gsub(/\{issue \w+\}/, "[unknown issue]")
|
18
|
+
%>
|
19
|
+
|
20
|
+
<table>
|
21
|
+
<tr>
|
22
|
+
<td class="attrname">Id:</td>
|
23
|
+
<td class="attrval"><%= issue.id %></td>
|
24
|
+
</tr>
|
25
|
+
|
26
|
+
<tr>
|
27
|
+
<td class="attrname">Type:</td>
|
28
|
+
<td class="attrval"><%= issue.type %></td>
|
29
|
+
</tr>
|
30
|
+
|
31
|
+
<tr>
|
32
|
+
<td class="attrname">Creation time:</td>
|
33
|
+
<td class="attrval"><%= issue.creation_time %></td>
|
34
|
+
</tr>
|
35
|
+
|
36
|
+
<tr>
|
37
|
+
<td class="attrname">Creator:</td>
|
38
|
+
<td class="attrval"><%=obscured_email issue.reporter %></td>
|
39
|
+
</tr>
|
40
|
+
|
41
|
+
<% unless issue.references.empty? %>
|
42
|
+
<tr>
|
43
|
+
<td class="attrname">References:</td>
|
44
|
+
<td class="attrval">
|
45
|
+
<% issue.references.each_with_index do |r, i| %>
|
46
|
+
[<%= i + 1 %>] <%= link_to r, r %><br/>
|
47
|
+
<% end %>
|
48
|
+
</td>
|
49
|
+
</tr>
|
50
|
+
|
51
|
+
<% end %>
|
52
|
+
|
53
|
+
<tr>
|
54
|
+
<td class="attrname">Release:</td>
|
55
|
+
<td class="attrval">
|
56
|
+
<% if release %>
|
57
|
+
<%= link_to release, release.name %>
|
58
|
+
<% if release.released? %>
|
59
|
+
(released <%= release.release_time.pretty_date %>)
|
60
|
+
<% else %>
|
61
|
+
(unreleased)
|
62
|
+
<% end %>
|
63
|
+
<% else %>
|
64
|
+
<%= link_to "unassigned", "unassigned" %>
|
65
|
+
<% end %>
|
66
|
+
</td>
|
67
|
+
</tr>
|
68
|
+
|
69
|
+
<tr>
|
70
|
+
<td class="attrname">Component:</td>
|
71
|
+
<td class="attrval"><%= link_to component, component.name %></td>
|
72
|
+
</tr>
|
73
|
+
|
74
|
+
<tr>
|
75
|
+
<td class="attrname">Status:</td>
|
76
|
+
<td class="attrval">
|
77
|
+
<%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
|
78
|
+
</td>
|
79
|
+
</tr>
|
80
|
+
</table>
|
81
|
+
|
82
|
+
<h2>Issue log</h2>
|
83
|
+
|
84
|
+
<table>
|
85
|
+
<% issue.log_events.each_with_index do |(time, who, what, comment), i| %>
|
86
|
+
<% if i % 2 == 0 %>
|
87
|
+
<tr class="logentryeven">
|
88
|
+
<% else %>
|
89
|
+
<tr class="logentryodd">
|
90
|
+
<% end %>
|
91
|
+
<td class="logtime"><%=h time %></td>
|
92
|
+
<td class="logwho"><%=obscured_email who %></td>
|
93
|
+
<td class="logwhat"><%=h what %></td>
|
94
|
+
</tr>
|
95
|
+
<tr><td colspan="3" class="logcomment">
|
96
|
+
<% if comment.empty? %>
|
97
|
+
<% else %>
|
98
|
+
<%=p comment %>
|
99
|
+
<% end %>
|
100
|
+
</td></tr>
|
101
|
+
<% end %>
|
102
|
+
</table>
|
103
|
+
|
104
|
+
<p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
|
105
|
+
</body>
|
106
|
+
</html>
|