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/Changelog +35 -0
- data/README.txt +127 -0
- data/Rakefile +33 -0
- data/ReleaseNotes +50 -0
- data/bin/ditz +213 -0
- data/contrib/completion/_ditz.zsh +29 -0
- data/contrib/completion/ditz.bash +22 -0
- data/lib/component.rhtml +22 -0
- data/lib/ditz.rb +56 -0
- data/lib/hook.rb +67 -0
- data/lib/html.rb +69 -0
- data/lib/index.rhtml +113 -0
- data/lib/issue.rhtml +111 -0
- data/lib/issue_table.rhtml +33 -0
- data/lib/lowline.rb +202 -0
- data/lib/model-objects.rb +314 -0
- data/lib/model.rb +208 -0
- data/lib/operator.rb +549 -0
- data/lib/plugins/git.rb +114 -0
- data/lib/plugins/issue-claiming.rb +92 -0
- data/lib/release.rhtml +69 -0
- data/lib/style.css +127 -0
- data/lib/trollop.rb +518 -0
- data/lib/unassigned.rhtml +31 -0
- data/lib/util.rb +57 -0
- data/lib/vendor/yaml_waml.rb +28 -0
- data/lib/view.rb +16 -0
- data/lib/views.rb +136 -0
- data/man/ditz.1 +38 -0
- metadata +90 -0
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'model'
|
2
|
+
|
3
|
+
module Ditz
|
4
|
+
|
5
|
+
class Component < ModelObject
|
6
|
+
field :name
|
7
|
+
def name_prefix; name.gsub(/\s+/, "-").downcase end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Release < ModelObject
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
field :name
|
14
|
+
field :status, :default => :unreleased, :ask => false
|
15
|
+
field :release_time, :ask => false
|
16
|
+
changes_are_logged
|
17
|
+
|
18
|
+
def released?; self.status == :released end
|
19
|
+
def unreleased?; !released? end
|
20
|
+
|
21
|
+
def issues_from project; project.issues.select { |i| i.release == name } end
|
22
|
+
|
23
|
+
def release! project, who, comment
|
24
|
+
raise Error, "already released" if released?
|
25
|
+
|
26
|
+
issues = issues_from project
|
27
|
+
bad = issues.find { |i| i.open? }
|
28
|
+
raise Error, "open issue #{bad.name} must be reassigned" if bad
|
29
|
+
|
30
|
+
self.release_time = Time.now
|
31
|
+
self.status = :released
|
32
|
+
log "released", who, comment
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Project < ModelObject
|
37
|
+
class Error < StandardError; end
|
38
|
+
|
39
|
+
attr_accessor :pathname
|
40
|
+
|
41
|
+
field :name, :default_generator => lambda { File.basename(Dir.pwd) }
|
42
|
+
field :version, :default => Ditz::VERSION, :ask => false
|
43
|
+
field :components, :multi => true, :generator => :get_components
|
44
|
+
field :releases, :multi => true, :ask => false
|
45
|
+
|
46
|
+
## issues are not model fields proper, so we build up their interface here.
|
47
|
+
attr_accessor :issues
|
48
|
+
def add_issue issue; added_issues << issue; issues << issue end
|
49
|
+
def drop_issue issue; deleted_issues << issue if issues.delete issue end
|
50
|
+
def added_issues; @added_issues ||= [] end
|
51
|
+
def deleted_issues; @deleted_issues ||= [] end
|
52
|
+
|
53
|
+
def get_components
|
54
|
+
puts <<EOS
|
55
|
+
Issues can be tracked across the project as a whole, or the project can be
|
56
|
+
split into components, and issues tracked separately for each component.
|
57
|
+
EOS
|
58
|
+
use_components = ask_yon "Track issues separately for different components?"
|
59
|
+
comp_names = use_components ? ask_for_many("components") : []
|
60
|
+
|
61
|
+
([name] + comp_names).uniq.map { |n| Component.create_interactively :with => { :name => n } }
|
62
|
+
end
|
63
|
+
|
64
|
+
def issues_for ident
|
65
|
+
by_name = issues.find { |i| i.name == ident }
|
66
|
+
by_name ? [by_name] : issues.select { |i| i.id =~ /^#{ident}/ }
|
67
|
+
end
|
68
|
+
|
69
|
+
def component_for component_name
|
70
|
+
components.find { |i| i.name == component_name }
|
71
|
+
end
|
72
|
+
|
73
|
+
def release_for release_name
|
74
|
+
releases.find { |i| i.name == release_name }
|
75
|
+
end
|
76
|
+
|
77
|
+
def unreleased_releases; releases.select { |r| r.unreleased? } end
|
78
|
+
|
79
|
+
def issues_for_release release
|
80
|
+
release == :unassigned ? unassigned_issues : issues.select { |i| i.release == release.name }
|
81
|
+
end
|
82
|
+
|
83
|
+
def issues_for_component component
|
84
|
+
issues.select { |i| i.component == component.name }
|
85
|
+
end
|
86
|
+
|
87
|
+
def unassigned_issues
|
88
|
+
issues.select { |i| i.release.nil? }
|
89
|
+
end
|
90
|
+
|
91
|
+
def group_issues these_issues=issues
|
92
|
+
these_issues.group_by { |i| i.type }.sort_by { |(t,g)| Issue::TYPE_ORDER[t] }
|
93
|
+
end
|
94
|
+
|
95
|
+
def assign_issue_names!
|
96
|
+
prefixes = components.map { |c| [c.name, c.name.gsub(/^\s+/, "-").downcase] }.to_h
|
97
|
+
ids = components.map { |c| [c.name, 0] }.to_h
|
98
|
+
issues.sort_by { |i| i.creation_time }.each do |i|
|
99
|
+
i.name = "#{prefixes[i.component]}-#{ids[i.component] += 1}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate!
|
104
|
+
if(dup = components.map { |c| c.name }.first_duplicate)
|
105
|
+
raise Error, "more than one component named #{dup.inspect}: #{components.inspect}"
|
106
|
+
elsif(dup = releases.map { |r| r.name }.first_duplicate)
|
107
|
+
raise Error, "more than one release named #{dup.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Issue < ModelObject
|
113
|
+
class Error < StandardError; end
|
114
|
+
|
115
|
+
field :title
|
116
|
+
field :desc, :prompt => "Description", :multiline => true
|
117
|
+
field :type, :generator => :get_type
|
118
|
+
field :component, :generator => :get_component
|
119
|
+
field :release, :generator => :get_release
|
120
|
+
field :reporter, :prompt => "Issue creator", :default_generator => lambda { |config, proj| config.user }
|
121
|
+
field :status, :ask => false, :default => :unstarted
|
122
|
+
field :disposition, :ask => false
|
123
|
+
field :creation_time, :ask => false, :generator => lambda { Time.now }
|
124
|
+
field :references, :ask => false, :multi => true
|
125
|
+
field :id, :ask => false, :generator => :make_id
|
126
|
+
changes_are_logged
|
127
|
+
|
128
|
+
attr_accessor :name, :pathname, :project
|
129
|
+
|
130
|
+
## these are the fields we interpolate issue names on
|
131
|
+
INTERPOLATED_FIELDS = [:title, :desc, :log_events]
|
132
|
+
|
133
|
+
STATUS_SORT_ORDER = { :unstarted => 2, :paused => 1, :in_progress => 0, :closed => 3 }
|
134
|
+
STATUS_WIDGET = { :unstarted => "_", :in_progress => ">", :paused => "=", :closed => "x" }
|
135
|
+
DISPOSITIONS = [ :fixed, :wontfix, :reorg ]
|
136
|
+
TYPES = [ :bugfix, :feature, :task ]
|
137
|
+
TYPE_ORDER = { :bugfix => 0, :feature => 1, :task => 2 }
|
138
|
+
TYPE_LETTER = { 'b' => :bugfix, 'f' => :feature, 't' => :task }
|
139
|
+
STATUSES = STATUS_WIDGET.keys
|
140
|
+
|
141
|
+
STATUS_STRINGS = { :in_progress => "in progress", :wontfix => "won't fix" }
|
142
|
+
DISPOSITION_STRINGS = { :wontfix => "won't fix", :reorg => "reorganized" }
|
143
|
+
|
144
|
+
def serialized_form_of field, value
|
145
|
+
return super unless INTERPOLATED_FIELDS.member? field
|
146
|
+
|
147
|
+
if field == :log_events
|
148
|
+
value.map do |time, who, what, comment|
|
149
|
+
comment = @project.issues.inject(comment) do |s, i|
|
150
|
+
s.gsub(/\b#{i.name}\b/, "{issue #{i.id}}")
|
151
|
+
end
|
152
|
+
[time, who, what, comment]
|
153
|
+
end
|
154
|
+
else
|
155
|
+
@project.issues.inject(value) do |s, i|
|
156
|
+
s.gsub(/\b#{i.name}\b/, "{issue #{i.id}}")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def deserialized_form_of field, value
|
162
|
+
return super unless INTERPOLATED_FIELDS.member? field
|
163
|
+
|
164
|
+
if field == :log_events
|
165
|
+
value.map do |time, who, what, comment|
|
166
|
+
comment = @project.issues.inject(comment) do |s, i|
|
167
|
+
s.gsub(/\{issue #{i.id}\}/, i.name)
|
168
|
+
end.gsub(/\{issue \w+\}/, "[unknown issue]")
|
169
|
+
[time, who, what, comment]
|
170
|
+
end
|
171
|
+
else
|
172
|
+
@project.issues.inject(value) do |s, i|
|
173
|
+
s.gsub(/\{issue #{i.id}\}/, i.name)
|
174
|
+
end.gsub(/\{issue \w+\}/, "[unknown issue]")
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
## make a unique id
|
179
|
+
def make_id config, project
|
180
|
+
SHA1.hexdigest [Time.now, rand, creation_time, reporter, title, desc].join("\n")
|
181
|
+
end
|
182
|
+
|
183
|
+
def sort_order; [STATUS_SORT_ORDER[status], creation_time] end
|
184
|
+
def status_widget; STATUS_WIDGET[status] end
|
185
|
+
|
186
|
+
def status_string; STATUS_STRINGS[status] || status.to_s end
|
187
|
+
def disposition_string; DISPOSITION_STRINGS[disposition] || disposition.to_s end
|
188
|
+
|
189
|
+
def closed?; status == :closed end
|
190
|
+
def open?; !closed? end
|
191
|
+
def in_progress?; status == :in_progress end
|
192
|
+
def unstarted?; !in_progress? end
|
193
|
+
def bug?; type == :bugfix end
|
194
|
+
def feature?; type == :feature end
|
195
|
+
def unassigned?; release.nil? end
|
196
|
+
def assigned?; !unassigned? end
|
197
|
+
|
198
|
+
def start_work who, comment; change_status :in_progress, who, comment end
|
199
|
+
def stop_work who, comment
|
200
|
+
raise Error, "unstarted" unless self.status == :in_progress
|
201
|
+
change_status :paused, who, comment
|
202
|
+
end
|
203
|
+
|
204
|
+
def close disp, who, comment
|
205
|
+
raise Error, "unknown disposition #{disp}" unless DISPOSITIONS.member? disp
|
206
|
+
log "closed with disposition #{disp}", who, comment
|
207
|
+
self.status = :closed
|
208
|
+
self.disposition = disp
|
209
|
+
end
|
210
|
+
|
211
|
+
def change_status to, who, comment
|
212
|
+
raise Error, "unknown status #{to}" unless STATUSES.member? to
|
213
|
+
raise Error, "already marked as #{to}" if status == to
|
214
|
+
log "changed status from #{status} to #{to}", who, comment
|
215
|
+
self.status = to
|
216
|
+
end
|
217
|
+
private :change_status
|
218
|
+
|
219
|
+
def change hash, who, comment
|
220
|
+
what = []
|
221
|
+
if title != hash[:title]
|
222
|
+
what << "changed title"
|
223
|
+
self.title = hash[:title]
|
224
|
+
end
|
225
|
+
|
226
|
+
if desc != hash[:description]
|
227
|
+
what << "changed description"
|
228
|
+
self.desc = hash[:description]
|
229
|
+
end
|
230
|
+
|
231
|
+
if reporter != hash[:reporter]
|
232
|
+
what << "changed reporter"
|
233
|
+
self.reporter = hash[:reporter]
|
234
|
+
end
|
235
|
+
|
236
|
+
unless what.empty?
|
237
|
+
log what.join(", "), who, comment
|
238
|
+
true
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def assign_to_release release, who, comment
|
243
|
+
log "assigned to release #{release.name} from #{self.release || 'unassigned'}", who, comment
|
244
|
+
self.release = release.name
|
245
|
+
end
|
246
|
+
|
247
|
+
def assign_to_component component, who, comment
|
248
|
+
log "assigned to component #{component.name} from #{self.component}", who, comment
|
249
|
+
self.component = component.name
|
250
|
+
end
|
251
|
+
|
252
|
+
def unassign who, comment
|
253
|
+
raise Error, "not assigned to a release" unless release
|
254
|
+
log "unassigned from release #{release}", who, comment
|
255
|
+
self.release = nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def get_type config, project
|
259
|
+
type = ask "Is this a (b)ugfix, a (f)eature, or a (t)ask?", :restrict => /^[bft]$/
|
260
|
+
TYPE_LETTER[type]
|
261
|
+
end
|
262
|
+
|
263
|
+
def get_component config, project
|
264
|
+
if project.components.size == 1
|
265
|
+
project.components.first
|
266
|
+
else
|
267
|
+
ask_for_selection project.components, "component", :name
|
268
|
+
end.name
|
269
|
+
end
|
270
|
+
|
271
|
+
def get_release config, project
|
272
|
+
releases = project.releases.select { |r| r.unreleased? }
|
273
|
+
if !releases.empty? && ask_yon("Assign to a release now?")
|
274
|
+
if releases.size == 1
|
275
|
+
r = releases.first
|
276
|
+
puts "Assigning to release #{r.name}."
|
277
|
+
r
|
278
|
+
else
|
279
|
+
ask_for_selection releases, "release", :name
|
280
|
+
end.name
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def get_reporter config, project
|
285
|
+
reporter = ask "Creator", :default => config.user
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class Config < ModelObject
|
290
|
+
field :name, :prompt => "Your name", :default_generator => :get_default_name
|
291
|
+
field :email, :prompt => "Your email address", :default_generator => :get_default_email
|
292
|
+
field :issue_dir, :ask => false, :default => "bugs"
|
293
|
+
|
294
|
+
def user; "#{name} <#{email}>" end
|
295
|
+
|
296
|
+
def get_default_name
|
297
|
+
require 'etc'
|
298
|
+
|
299
|
+
name = Etc.getpwnam(ENV["USER"])
|
300
|
+
name = name ? name.gecos.split(/,/).first : ""
|
301
|
+
end
|
302
|
+
|
303
|
+
def get_default_email
|
304
|
+
require 'socket'
|
305
|
+
email = (ENV["USER"] || "") + "@" +
|
306
|
+
begin
|
307
|
+
Socket.gethostbyname(Socket.gethostname).first
|
308
|
+
rescue SocketError
|
309
|
+
Socket.gethostname
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
data/lib/model.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'sha1'
|
3
|
+
require "lowline"; include Lowline
|
4
|
+
require "util"
|
5
|
+
|
6
|
+
class Time
|
7
|
+
alias :old_to_yaml :to_yaml
|
8
|
+
def to_yaml(opts = {})
|
9
|
+
self.utc.old_to_yaml(opts)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Ditz
|
14
|
+
|
15
|
+
class ModelObject
|
16
|
+
class ModelError < StandardError; end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@values = {}
|
20
|
+
@serialized_values = {}
|
21
|
+
self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
|
22
|
+
end
|
23
|
+
|
24
|
+
## yamlability
|
25
|
+
def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
|
26
|
+
def self.yaml_other_thing; name.split('::').last.dcfirst end
|
27
|
+
def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
|
28
|
+
def self.inherited subclass
|
29
|
+
YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
|
30
|
+
o = subclass.new
|
31
|
+
val.each do |k, v|
|
32
|
+
m = "__serialized_#{k}="
|
33
|
+
if o.respond_to? m
|
34
|
+
o.send m, v
|
35
|
+
else
|
36
|
+
$stderr.puts "warning: unknown field #{k.inspect} in YAML for #{type}; ignoring"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
o.unchanged!
|
40
|
+
o
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
## override these two to model per-field transformations between disk and
|
45
|
+
## memory.
|
46
|
+
##
|
47
|
+
## convert disk form => memory form
|
48
|
+
def deserialized_form_of field, value
|
49
|
+
@serialized_values[field]
|
50
|
+
end
|
51
|
+
|
52
|
+
## convert memory form => disk form
|
53
|
+
def serialized_form_of field, value
|
54
|
+
@values[field]
|
55
|
+
end
|
56
|
+
|
57
|
+
## add a new field to a model object
|
58
|
+
def self.field name, opts={}
|
59
|
+
@fields ||= [] # can't use a hash because we need to preserve field order
|
60
|
+
raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
|
61
|
+
@fields << [name, opts]
|
62
|
+
|
63
|
+
if opts[:multi]
|
64
|
+
single_name = name.to_s.sub(/s$/, "") # oh yeah
|
65
|
+
define_method "add_#{single_name}" do |obj|
|
66
|
+
array = send(name)
|
67
|
+
raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
|
68
|
+
changed!
|
69
|
+
@serialized_values.delete name
|
70
|
+
array << obj
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "drop_#{single_name}" do |obj|
|
74
|
+
return unless @values[name].delete obj
|
75
|
+
@serialized_values.delete name
|
76
|
+
changed!
|
77
|
+
obj
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
define_method "#{name}=" do |o|
|
82
|
+
changed!
|
83
|
+
@serialized_values.delete name
|
84
|
+
@values[name] = o
|
85
|
+
end
|
86
|
+
|
87
|
+
define_method "__serialized_#{name}=" do |o|
|
88
|
+
changed!
|
89
|
+
@values.delete name
|
90
|
+
@serialized_values[name] = o
|
91
|
+
end
|
92
|
+
|
93
|
+
define_method name do
|
94
|
+
return @values[name] if @values.member?(name)
|
95
|
+
@values[name] = deserialized_form_of name, @serialized_values[name]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.field_names; @fields.map { |name, opts| name } end
|
100
|
+
class << self
|
101
|
+
attr_reader :fields, :values, :serialized_values
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.changes_are_logged
|
105
|
+
define_method(:changes_are_logged?) { true }
|
106
|
+
field :log_events, :multi => true, :ask => false
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.from fn
|
110
|
+
returning YAML::load_file(fn) do |o|
|
111
|
+
raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
|
112
|
+
o.pathname = fn if o.respond_to? :pathname=
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
"<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + (@values[f].to_s || @serialized_values[f]).inspect }.join(", ") + ">"
|
118
|
+
end
|
119
|
+
|
120
|
+
def inspect; to_s end
|
121
|
+
|
122
|
+
## depth-first search on all reachable ModelObjects. fuck yeah.
|
123
|
+
def each_modelobject
|
124
|
+
seen = {}
|
125
|
+
to_see = [self]
|
126
|
+
until to_see.empty?
|
127
|
+
cur = to_see.pop
|
128
|
+
seen[cur] = true
|
129
|
+
yield cur
|
130
|
+
cur.class.field_names.each do |f|
|
131
|
+
val = cur.send(f)
|
132
|
+
next if seen[val]
|
133
|
+
if val.is_a?(ModelObject)
|
134
|
+
to_see.push val
|
135
|
+
elsif val.is_a?(Array)
|
136
|
+
to_see += val.select { |v| v.is_a?(ModelObject) }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def save! fn
|
143
|
+
#FileUtils.mv fn, "#{fn}~", :force => true rescue nil
|
144
|
+
File.open(fn, "w") { |f| f.puts to_yaml }
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_yaml opts={}
|
149
|
+
ret = YAML::quick_emit(object_id, opts) do |out|
|
150
|
+
out.map(taguri, nil) do |map|
|
151
|
+
self.class.fields.each do |f, fops|
|
152
|
+
v = if @serialized_values.member?(f)
|
153
|
+
@serialized_values[f]
|
154
|
+
else
|
155
|
+
@serialized_values[f] = serialized_form_of f, @values[f]
|
156
|
+
end
|
157
|
+
|
158
|
+
map.add f.to_s, v
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
ret.decode
|
163
|
+
end
|
164
|
+
|
165
|
+
def log what, who, comment
|
166
|
+
add_log_event([Time.now, who, what, comment || ""])
|
167
|
+
self
|
168
|
+
end
|
169
|
+
|
170
|
+
def changed?; @changed ||= false end
|
171
|
+
def changed!; @changed = true end
|
172
|
+
def unchanged!; @changed = false end
|
173
|
+
|
174
|
+
def self.create_interactively opts={}
|
175
|
+
o = self.new
|
176
|
+
args = opts[:args] || []
|
177
|
+
@fields.each do |name, field_opts|
|
178
|
+
val = if opts[:with] && opts[:with][name]
|
179
|
+
opts[:with][name]
|
180
|
+
elsif field_opts[:generator].is_a? Proc
|
181
|
+
field_opts[:generator].call(*args)
|
182
|
+
elsif field_opts[:generator]
|
183
|
+
o.send field_opts[:generator], *args
|
184
|
+
elsif field_opts[:ask] == false # nil counts as true here
|
185
|
+
field_opts[:default] || (field_opts[:multi] ? [] : nil)
|
186
|
+
else
|
187
|
+
q = field_opts[:prompt] || name.to_s.capitalize
|
188
|
+
if field_opts[:multiline]
|
189
|
+
ask_multiline q
|
190
|
+
else
|
191
|
+
default = if field_opts[:default_generator].is_a? Proc
|
192
|
+
field_opts[:default_generator].call(*args)
|
193
|
+
elsif field_opts[:default_generator]
|
194
|
+
o.send field_opts[:default_generator], *args
|
195
|
+
elsif field_opts[:default]
|
196
|
+
field_opts[:default]
|
197
|
+
end
|
198
|
+
|
199
|
+
ask q, :default => default
|
200
|
+
end
|
201
|
+
end
|
202
|
+
o.send("#{name}=", val)
|
203
|
+
end
|
204
|
+
o
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|