shiftzilla 0.2.6

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,28 @@
1
+ require 'shiftzilla/milestone'
2
+
3
+ module Shiftzilla
4
+ class Milestones
5
+ def initialize(msrc)
6
+ @milestones = {}
7
+ msrc.each do |key,val|
8
+ @milestones[key.to_sym] = Shiftzilla::Milestone.new(val)
9
+ end
10
+ end
11
+
12
+ def start
13
+ @milestones[:start]
14
+ end
15
+
16
+ def feature_complete
17
+ @milestones[:feature_complete]
18
+ end
19
+
20
+ def code_freeze
21
+ @milestones[:code_freeze]
22
+ end
23
+
24
+ def ga
25
+ @milestones[:ga]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,332 @@
1
+ require 'json'
2
+ require 'shiftzilla/bug'
3
+ require 'shiftzilla/helpers'
4
+ require 'shiftzilla/team_data'
5
+
6
+ include Shiftzilla::Helpers
7
+
8
+ module Shiftzilla
9
+ class OrgData
10
+ attr_reader :tmp_dir
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ @teams = config.teams
15
+ @releases = config.releases
16
+ @tmp_dir = Shiftzilla::Helpers.tmp_dir
17
+ @org_data = { '_overall' => Shiftzilla::TeamData.new('_overall') }
18
+ end
19
+
20
+ def populate_releases
21
+ all_snapshots.each do |snapshot|
22
+ snapdate = Date.parse(snapshot)
23
+ next if snapdate < @config.earliest_milestone
24
+ break if snapdate > @config.latest_milestone
25
+ break if snapdate > Date.today
26
+
27
+ dbh.execute(all_bugs_query(snapshot)) do |row|
28
+ bzid = row[0].strip
29
+ comp = row[1].strip
30
+ tgtr = row[2].strip
31
+ owns = row[3].strip
32
+ stat = row[4].strip
33
+ summ = row[5].strip
34
+ keyw = row[6].nil? ? '' : row[6].strip
35
+ pmsc = row[7].nil? ? '0' : row[7].strip
36
+ cust = row[8].nil? ? 0 : row[8]
37
+
38
+ # Package up bug data
39
+ binfo = {
40
+ :snapdate => snapdate,
41
+ :test_blocker => keyw.include?('TestBlocker'),
42
+ :ops_blocker => keyw.include?('OpsBlocker'),
43
+ :owner => owns,
44
+ :summary => summ,
45
+ :component => comp,
46
+ :pm_score => pmsc,
47
+ :cust_cases => (cust == 1),
48
+ :tgt_release => tgtr,
49
+ }
50
+
51
+ tgt_release = @config.release_by_target(tgtr)
52
+ all_release = @config.release('All')
53
+
54
+ # If this component isn't mapped to a team, stub out a fake team.
55
+ tname = comp_map.has_key?(comp) ? comp_map[comp] : "(?) #{comp}"
56
+ unless @org_data.has_key?(tname)
57
+ @config.add_ad_hoc_team({ 'name' => tname, 'components' => [comp] })
58
+ @org_data[tname] = Shiftzilla::TeamData.new(tname)
59
+ end
60
+
61
+ team_rdata = tgt_release.nil? ? nil : @org_data[tname].get_release_data(tgt_release)
62
+ team_adata = @org_data[tname].get_release_data(all_release)
63
+ over_rdata = tgt_release.nil? ? nil : @org_data['_overall'].get_release_data(tgt_release)
64
+ over_adata = @org_data['_overall'].get_release_data(all_release)
65
+
66
+ # Do some bean counting
67
+ [over_rdata,team_rdata,over_adata,team_adata].each do |group|
68
+ next if group.nil?
69
+ snapdata = group.get_snapdata(snapshot)
70
+ if group.first_snap.nil?
71
+ group.first_snap = snapshot
72
+ group.first_snapdate = snapdate
73
+ end
74
+ group.latest_snap = snapshot
75
+ group.latest_snapdate = snapdate
76
+
77
+ bug = group.add_or_update_bug(bzid,binfo)
78
+
79
+ # Add info to the snapshot
80
+ snapdata.bug_ids << bzid
81
+ if bug.test_blocker or bug.ops_blocker
82
+ snapdata.tb_ids << bzid
83
+ end
84
+ if bug.cust_cases
85
+ snapdata.cc_ids << bzid
86
+ end
87
+ end
88
+ end
89
+
90
+ # Determine new and closed bug counts by comparing the current
91
+ # snapshot to the previous snapshot
92
+ @org_data.each do |tname,tdata|
93
+ @releases.each do |release|
94
+ next unless tdata.has_release_data?(release)
95
+
96
+ rdata = tdata.get_release_data(release)
97
+ next unless rdata.has_snapdata?(snapshot)
98
+
99
+ if rdata.prev_snap.nil?
100
+ rdata.prev_snap = snapshot
101
+ next
102
+ end
103
+
104
+ currdata = rdata.get_snapdata(snapshot)
105
+ prevdata = rdata.get_snapdata(rdata.prev_snap)
106
+
107
+ prev_bzids = prevdata.bug_ids
108
+ curr_bzids = currdata.bug_ids
109
+ prev_tbids = prevdata.tb_ids
110
+ curr_tbids = currdata.tb_ids
111
+ prev_ccids = prevdata.cc_ids
112
+ curr_ccids = currdata.cc_ids
113
+ currdata.closed_bugs = prev_bzids.select{ |bzid| not curr_bzids.include?(bzid) }.length
114
+ currdata.new_bugs = curr_bzids.select{ |bzid| not prev_bzids.include?(bzid) }.length
115
+ currdata.closed_tb = prev_tbids.select{ |tbid| not curr_tbids.include?(tbid) }.length
116
+ currdata.new_tb = curr_tbids.select{ |tbid| not prev_tbids.include?(tbid) }.length
117
+ currdata.closed_cc = prev_ccids.select{ |ccid| not curr_ccids.include?(ccid) }.length
118
+ currdata.new_cc = curr_ccids.select{ |ccid| not prev_ccids.include?(ccid) }.length
119
+
120
+ rdata.prev_snap = snapshot
121
+ end
122
+ end
123
+ end
124
+ @all_teams = @teams.map{ |t| t.name }.concat(@org_data.keys).uniq
125
+ @ordered_teams = ['_overall'].concat(@all_teams.select{ |t| t != '_overall'}.sort)
126
+ end
127
+
128
+ def get_ordered_teams
129
+ @ordered_teams
130
+ end
131
+
132
+ def get_release_data(team_name,release)
133
+ if @org_data.has_key?(team_name) and @org_data[team_name].has_release_data?(release)
134
+ return @org_data[team_name].get_release_data(release)
135
+ end
136
+ return nil
137
+ end
138
+
139
+ def build_series
140
+ @team_files = []
141
+ @ordered_teams.each do |tname|
142
+ unless @org_data.has_key?(tname)
143
+ @org_data[tname] = Shiftzilla::TeamData.new(tname)
144
+ end
145
+ tdata = @org_data[tname]
146
+ tinfo = @config.team(tname)
147
+
148
+ @team_files << {
149
+ :tname => tdata.title,
150
+ :file => tdata.file,
151
+ :releases => {},
152
+ }
153
+
154
+ @releases.each do |release|
155
+ rname = release.name
156
+ bug_total = 0
157
+ unless tdata.has_release_data?(release)
158
+ @team_files[-1][:releases][release.name] = bug_total
159
+ next
160
+ end
161
+ rdata = tdata.get_release_data(release)
162
+ if rdata.snaps.has_key?(latest_snapshot)
163
+ bug_total = rdata.snaps[latest_snapshot].total_bugs
164
+ end
165
+ @team_files[-1][:releases][release.name] = bug_total
166
+
167
+ next if rdata.first_snap.nil? and not release.uses_milestones?
168
+ next if release.built_in?
169
+
170
+ rdata.populate_series
171
+ end
172
+ end
173
+ end
174
+
175
+ def generate_reports
176
+ build_time = timestamp
177
+ all_release = @config.release('All')
178
+ @ordered_teams.each do |tname|
179
+ tinfo = @config.team(tname)
180
+ tdata = @org_data[tname]
181
+
182
+ team_pinfo = {
183
+ :build_time => build_time,
184
+ :tname => tdata.title,
185
+ :tinfo => tinfo,
186
+ :tdata => tdata,
187
+ :team_files => @team_files,
188
+ :bug_url => BZ_URL,
189
+ :chart_order => ((1..(@releases.length - 1)).to_a << 0),
190
+ :releases => [],
191
+ :latest_snapshot => latest_snapshot,
192
+ :all_bugs => [],
193
+ }
194
+
195
+ @releases.each do |release|
196
+ rname = release.name
197
+ rdata = tdata.has_release_data?(release) ? tdata.get_release_data(release) : nil
198
+ snapdata = nil
199
+ if not rdata.nil? and rdata.snaps.has_key?(latest_snapshot)
200
+ snapdata = rdata.snaps[latest_snapshot]
201
+ else
202
+ snapdata = Shiftzilla::SnapData.new(latest_snapshot)
203
+ end
204
+
205
+ release_info = {
206
+ :release => release,
207
+ :snapdata => snapdata,
208
+ :no_rdata => (rdata.nil? ? true : false),
209
+ :bug_avg_age => (rdata.nil? ? 0 : rdata.bug_avg_age),
210
+ :tb_avg_age => (rdata.nil? ? 0 : rdata.tb_avg_age),
211
+ }
212
+ unless rdata.nil?
213
+ release_info[:charts] = {
214
+ :burndown => chartify('Bug Burndown',rdata.series[:date],[
215
+ {
216
+ :label => 'Ideal Trend',
217
+ :data => rdata.series[:ideal],
218
+ },
219
+ {
220
+ :label => 'Total',
221
+ :data => rdata.series[:total_bugs],
222
+ },
223
+ {
224
+ :label => 'w/ Customer Cases',
225
+ :data => rdata.series[:total_cc],
226
+ },
227
+ ]),
228
+ :new_closed => chartify('New vs. Closed',rdata.series[:date],[
229
+ {
230
+ :label => 'New',
231
+ :data => rdata.series[:new_bugs],
232
+ },
233
+ {
234
+ :label => 'Closed',
235
+ :data => rdata.series[:closed_bugs],
236
+ },
237
+ ]),
238
+ :blockers => chartify('Test / Ops Blockers',rdata.series[:date],[
239
+ {
240
+ :label => 'Total',
241
+ :data => rdata.series[:total_tb],
242
+ },
243
+ {
244
+ :label => 'New',
245
+ :data => rdata.series[:new_tb],
246
+ },
247
+ {
248
+ :label => 'Closed',
249
+ :data => rdata.series[:closed_tb],
250
+ },
251
+ ]),
252
+ }
253
+ end
254
+ team_pinfo[:releases] << release_info
255
+
256
+ if rname == 'All'
257
+ team_pinfo[:all_bugs] = snapdata.bug_ids.map{ |id| rdata.bugs[id] }
258
+ end
259
+ end
260
+ team_page = haml_engine.render(Object.new,team_pinfo)
261
+ File.write(File.join(@tmp_dir,tdata.file), team_page)
262
+ end
263
+ # Copy flot library to build area
264
+ jsdir = File.join(@tmp_dir,'js')
265
+ Dir.mkdir(jsdir)
266
+ FileUtils.cp(File.join(VENDOR_DIR,'flot','jquery.flot.min.js'),jsdir)
267
+ end
268
+
269
+ def show_local_reports
270
+ system("open file://#{@tmp_dir}/index.html")
271
+ end
272
+
273
+ def publish_reports(ssh)
274
+ system("ssh #{ssh[:host]} 'rm -rf #{ssh[:path]}/*'")
275
+ system("rsync -avPq #{@tmp_dir}/* #{ssh[:host]}:#{ssh[:path]}/")
276
+ end
277
+
278
+ private
279
+
280
+ def comp_map
281
+ @comp_map ||= begin
282
+ comp_map = {}
283
+ @teams.each do |team|
284
+ team.components.each do |comp|
285
+ comp_map[comp] = team.name
286
+ end
287
+ end
288
+ comp_map
289
+ end
290
+ end
291
+
292
+ def chartify(title,dates,series)
293
+ modulo = label_modulo(dates.length)
294
+ tlist = []
295
+ dates.each_with_index do |val,idx|
296
+ next unless (idx + 1) % modulo == 0
297
+ tlist << [idx,Date.parse(val).strftime('%m/%d')]
298
+ end
299
+ data = []
300
+ series.each do |s|
301
+ dlist = []
302
+ s[:data].each_with_index do |val,idx|
303
+ dlist <<[idx,val]
304
+ end
305
+ data << { :label => s[:label], :data => dlist }
306
+ end
307
+ options = {
308
+ :xaxis => {
309
+ :ticks => tlist,
310
+ },
311
+ :yaxis => {
312
+ :min => 0,
313
+ :labelWidth => 50,
314
+ },
315
+ :tickSize => 5,
316
+ }
317
+ return { :title => title, :data => data.to_json, :options => options.to_json }
318
+ end
319
+
320
+ def label_modulo(date_count)
321
+ if date_count < 15
322
+ return 1
323
+ elsif date_count < 30
324
+ return 2
325
+ elsif date_count < 60
326
+ return 5
327
+ else
328
+ return 10
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,34 @@
1
+ require 'shiftzilla/milestones'
2
+
3
+ module Shiftzilla
4
+ class Release
5
+ attr_reader :name, :targets, :milestones, :default, :token
6
+
7
+ def initialize(release,builtin=false)
8
+ @name = release['name']
9
+ @token = @name.tr(' .', '_')
10
+ @targets = release['targets']
11
+ @default = release.has_key?('default') ? release['default'] : false
12
+ @builtin = builtin
13
+ @milestones = nil
14
+ if release.has_key?('milestones')
15
+ @milestones = Shiftzilla::Milestones.new(release['milestones'])
16
+ end
17
+ end
18
+
19
+ def uses_milestones?
20
+ return @milestones.nil? ? false : true
21
+ end
22
+
23
+ def built_in?
24
+ @builtin
25
+ end
26
+
27
+ def no_tgt_rel?
28
+ if @targets.length == 1 and @targets[0] == '---'
29
+ return true
30
+ end
31
+ false
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,185 @@
1
+ require 'shiftzilla/bug'
2
+ require 'shiftzilla/snap_data'
3
+
4
+ module Shiftzilla
5
+ class ReleaseData
6
+ attr_accessor :snaps, :bugs, :prev_snap, :latest_snap, :latest_snapdate, :first_snap, :first_snapdate, :labels, :series
7
+
8
+ def initialize(release)
9
+ @release = release
10
+ @snaps = {}
11
+ @bugs = {}
12
+ @prev_snap = nil
13
+ @latest_snap = nil
14
+ @latest_snapdate = nil
15
+ @first_snap = nil
16
+ @first_snapdate = nil
17
+ @labels = {}
18
+ @series = {
19
+ :date => [],
20
+ :ideal => [],
21
+ :total_bugs => [],
22
+ :new_bugs => [],
23
+ :closed_bugs => [],
24
+ :total_tb => [],
25
+ :new_tb => [],
26
+ :closed_tb => [],
27
+ :total_cc => [],
28
+ :new_cc => [],
29
+ :closed_cc => [],
30
+ }
31
+ end
32
+
33
+ def max_total
34
+ pick_max([@snaps.values.map{ |s| s.total_bugs }])
35
+ end
36
+
37
+ def max_new_closed
38
+ pick_max([@snaps.values.map{ |s| s.new_bugs },
39
+ @snaps.values.map{ |s| s.closed_bugs }
40
+ ])
41
+ end
42
+
43
+ def max_tb
44
+ pick_max([@snaps.values.map{ |s| s.total_tb },
45
+ @snaps.values.map{ |s| s.new_tb },
46
+ @snaps.values.map{ |s| s.closed_tb }
47
+ ])
48
+ end
49
+
50
+ def max_cc
51
+ pick_max([@snaps.values.map{ |s| s.total_cc },
52
+ @snaps.values.map{ |s| s.new_cc },
53
+ @snaps.values.map{ |s| s.closed_cc }
54
+ ])
55
+ end
56
+
57
+ def populate_series
58
+ return if first_snap.nil?
59
+
60
+ # Set up the 'ideal' series
61
+ ideal_slope = max_total.to_f / burndown_span.to_f
62
+ ideal_total = max_total
63
+
64
+ range_idx = 0
65
+ series_range.each do |date|
66
+ next if date.saturday? or date.sunday?
67
+ snapshot = date.strftime('%Y-%m-%d')
68
+
69
+ @series[:date] << snapshot
70
+ @series[:ideal] << ideal_total
71
+ ideal_total = ideal_total < ideal_slope ? 0 : ideal_total - ideal_slope
72
+
73
+ snapdata = @snaps.has_key?(snapshot) ? @snaps[snapshot] : nil
74
+ if date < first_snapdate
75
+ snapdata = @snaps[first_snap]
76
+ end
77
+
78
+ ['bugs','tb','cc'].each do |set|
79
+ ['total','new','closed'].each do |count|
80
+ set_key = "#{count}_#{set}".to_sym
81
+ @series[set_key] << (snapdata.nil? ? nil : snapdata.send(set_key))
82
+ end
83
+ end
84
+
85
+ if range_idx % label_modulo == 0
86
+ @labels[range_idx] = date.strftime('%m/%d')
87
+ end
88
+ range_idx += 1
89
+ end
90
+ end
91
+
92
+ def series_range
93
+ if @release.uses_milestones?
94
+ return (@release.milestones.start.date..@release.milestones.ga.date)
95
+ elsif not @first_snapdate.nil? and not @latest_snapdate.nil?
96
+ return (@first_snapdate..@latest_snapdate)
97
+ end
98
+ return nil
99
+ end
100
+
101
+ def series_span
102
+ if @release.uses_milestones?
103
+ return business_days_between(@release.milestones.start.date,@release.milestones.ga.date)
104
+ elsif not @first_snapdate.nil? and not @latest_snapdate.nil?
105
+ return business_days_between(@first_snapdate,@latest_snapdate)
106
+ end
107
+ return nil
108
+ end
109
+
110
+ def burndown_span
111
+ if @release.uses_milestones?
112
+ return business_days_between(@release.milestones.start.date,@release.milestones.code_freeze.date) - 2
113
+ elsif not @first_snapdate.nil? and not @latest_snapdate.nil?
114
+ return business_days_between(@first_snapdate,@latest_snapdate)
115
+ end
116
+ return nil
117
+ end
118
+
119
+ def bug_avg_age(blockers_only=false)
120
+ bug_list = blockers_only ? @bugs.values.select{ |b| b.test_blocker } : @bugs.values
121
+ bug_count = bug_list.length
122
+ age_total = bug_list.map{ |b| b.age }.sum
123
+ return bug_count == 0 ? 0 : (age_total / bug_count).round(1).to_s
124
+ end
125
+
126
+ def tb_avg_age
127
+ bug_avg_age(true)
128
+ end
129
+
130
+ def has_snapdata?(snapshot)
131
+ @snaps.has_key?(snapshot)
132
+ end
133
+
134
+ def get_snapdata(snapshot)
135
+ unless @snaps.has_key?(snapshot)
136
+ @snaps[snapshot] = Shiftzilla::SnapData.new(snapshot)
137
+ end
138
+ @snaps[snapshot]
139
+ end
140
+
141
+ def add_or_update_bug(bzid,binfo)
142
+ unless @bugs.has_key?(bzid)
143
+ @bugs[bzid] = Shiftzilla::Bug.new(bzid,binfo)
144
+ else
145
+ @bugs[bzid].update(binfo)
146
+ end
147
+ @bugs[bzid]
148
+ end
149
+
150
+ private
151
+
152
+ # Hat tip to https://stackoverflow.com/a/24753003 for this:
153
+ def business_days_between(start_date, end_date)
154
+ days_between = (end_date - start_date).to_i
155
+ return 0 unless days_between > 0
156
+ whole_weeks, extra_days = days_between.divmod(7)
157
+ unless extra_days.zero?
158
+ extra_days -= if (start_date + 1).wday <= end_date.wday
159
+ [(start_date + 1).sunday?, end_date.saturday?].count(true)
160
+ else
161
+ 2
162
+ end
163
+ end
164
+ (whole_weeks * 5) + extra_days
165
+ end
166
+
167
+ def pick_max(series_list)
168
+ max_val = 0
169
+ series_list.each do |s|
170
+ smax = s.select{ |v| not v.nil? }.max
171
+ next if smax.nil?
172
+ next unless smax > max_val
173
+ max_val = smax
174
+ end
175
+ return max_val
176
+ end
177
+
178
+ def label_modulo
179
+ if series_span < 50
180
+ return 5
181
+ end
182
+ return 10
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,31 @@
1
+ module Shiftzilla
2
+ class SnapData
3
+ attr_reader :id
4
+ attr_accessor :bug_ids, :tb_ids, :cc_ids, :new_bugs, :closed_bugs, :new_tb, :closed_tb, :new_cc, :closed_cc
5
+
6
+ def initialize(snapshot)
7
+ @id = snapshot
8
+ @bug_ids = []
9
+ @tb_ids = []
10
+ @cc_ids = []
11
+ @new_bugs = 0
12
+ @closed_bugs = 0
13
+ @new_tb = 0
14
+ @closed_tb = 0
15
+ @new_cc = 0
16
+ @closed_cc = 0
17
+ end
18
+
19
+ def total_bugs
20
+ @bug_ids.length
21
+ end
22
+
23
+ def total_tb
24
+ @tb_ids.length
25
+ end
26
+
27
+ def total_cc
28
+ @cc_ids.length
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ require 'shiftzilla/helpers'
2
+
3
+ module Shiftzilla
4
+ class Source
5
+ attr_reader :id, :table
6
+ def initialize(qid,qinfo)
7
+ @id = qid.to_sym
8
+ @search = qinfo['search']
9
+ @sharer = qinfo['sharer']
10
+ @table = qinfo['table']
11
+ @external_sub = qinfo['external_sub']
12
+ @fields = qinfo['fields'].map{ |f| f.to_sym }
13
+ @external_bugs_idx = @fields.index(:external_bugs)
14
+ end
15
+
16
+ def has_records_for_today?
17
+ count = 0
18
+ dbh.execute("SELECT count(*) FROM #{@table} WHERE Snapshot = date('now')") do |row|
19
+ count = row[0].to_i
20
+ end
21
+ return count > 0
22
+ end
23
+
24
+ def load_records
25
+ output_format = @fields.map{ |fld| "%{#{fld.to_s}}" }.join("\x1F")
26
+ table_fields = @fields.map{ |fld| "\"#{field_map[fld]}\"" }.join(',')
27
+ insert_frame = @fields.map{ |fld| '?' }.join(', ')
28
+ bz_command = "bugzilla query --savedsearch #{@search} --savedsearch-sharer-id=#{@sharer} --outputformat='#{output_format}'"
29
+ bz_csv = `#{bz_command}`
30
+ row_count = 0
31
+ bz_csv.split("\n").each do |row|
32
+ values = row.split("\x1F")
33
+ if not @external_bugs_idx.nil?
34
+ if not @external_sub.nil? and values[@external_bugs_idx].include?(@external_sub)
35
+ values[@external_bugs_idx] = 1
36
+ else
37
+ values[@external_bugs_idx] = 0
38
+ end
39
+ end
40
+ dbh.execute("INSERT INTO #{@table} (#{table_fields}) VALUES (#{insert_frame})", values)
41
+ row_count += 1
42
+ end
43
+ dbh.execute("UPDATE #{@table} SET Snapshot = date('now') WHERE Snapshot ISNULL")
44
+ return row_count
45
+ end
46
+
47
+ def purge_records
48
+ dbh.execute("DELETE FROM #{@table} WHERE Snapshot == date('now') OR Snapshot ISNULL")
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ module Shiftzilla
2
+ class Team
3
+ attr_reader :name, :lead, :group, :components
4
+
5
+ def initialize(tinfo,group_map,ad_hoc=false)
6
+ @name = tinfo['name']
7
+ @lead = ad_hoc ? nil : tinfo['lead']
8
+ @group = ad_hoc ? nil : group_map[tinfo['group']]
9
+ @components = tinfo.has_key?('components') ? tinfo['components'] : []
10
+ @ad_hoc = ad_hoc
11
+ end
12
+
13
+ def ad_hoc?
14
+ @ad_hoc
15
+ end
16
+ end
17
+ end