admin_assistant 0.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/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,43 @@
1
+ admin_assistant
2
+ ===============
3
+
4
+ admin_assistant is a Rails plugin that automates a lot of features typically
5
+ needed in admin interfaces. Current features include:
6
+
7
+ * Your basic CReate / Update / Delete
8
+ * Index with pagination and field ordering
9
+ * Search, either by all text fields or by specific fields
10
+ * Live querying of models to generate forms and indexes, meaning that adding
11
+ new columns to your admin controllers is easy
12
+ * Simple handling of belongs_to association via drop-down selects
13
+ * Built-in support for Paperclip and FileColumn
14
+
15
+ I'm following a few design principles in building this:
16
+
17
+ * admin_assistant's specs are written through an actual Rails app: I believe
18
+ this is the only sensible way to test a Rails plugin that deals with lots of
19
+ controller actions and views.
20
+ * admin_assistant will support multiple versions of Rails, so I'm experimenting
21
+ with a spec suite that can be run against all versions with one Rake task.
22
+ * admin_assistant will be severely hookable. If you're copying and pasting
23
+ something out of vendor/plugins/admin_assistant, that's a design flaw.
24
+ * admin_assistant will be minimally invasive to the rest of the Rails app. It
25
+ does not require that you add strange one-off methods to an important model
26
+ just to do something in an admin controller. And I'll try to avoid doing
27
+ anything silly like alias_method_chaining anything on ActionController::Base.
28
+ * admin_assistant will have some safe defaults, including turning off the
29
+ destroy action by default, and not filling in dates and times with the
30
+ current date or time (which is almost always useless).
31
+
32
+ There are also some features I'm skimping on right now:
33
+
34
+ * Super-pretty CSS: Because I suck at CSS. Submissions of themes are welcome
35
+ though.
36
+ * Super-fancy Ajax: Because I think it's easy to do this wrong. But there will
37
+ be some Ajax at some point I spose.
38
+
39
+ Basically, this plugin should act like a really great administrative assistant
40
+ in your office. It tries to be extremely helpful, but it won't get underfoot or
41
+ tell you how to do your job.
42
+
43
+ Copyright (c) 2009 Francis Hwang, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'spec/rake/spectask'
5
+
6
+ desc 'Default: run all specs across all supported Rails gem versions.'
7
+ task :default => :spec
8
+
9
+ desc 'Run all specs across all supported Rails gem versions.'
10
+ task :spec do
11
+ %w(2.1.2 2.2.2 2.3.2).each do |rails_gem_version|
12
+ puts "*** RAILS #{rails_gem_version} ***"
13
+ cmd = "cd test_rails_app && RAILS_GEM_VERSION=#{rails_gem_version} rake"
14
+ puts cmd
15
+ puts `#{cmd}`
16
+ puts
17
+ puts
18
+ end
19
+ end
20
+
21
+ desc 'Generate documentation for the admin_assistant plugin.'
22
+ Rake::RDocTask.new(:rdoc) do |rdoc|
23
+ rdoc.rdoc_dir = 'rdoc'
24
+ rdoc.title = 'AdminAssistant'
25
+ rdoc.options << '--line-numbers' << '--inline-source'
26
+ rdoc.rdoc_files.include('README')
27
+ rdoc.rdoc_files.include('lib/**/*.rb')
28
+ end
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,104 @@
1
+ class AdminAssistant
2
+ class Builder
3
+ attr_reader :admin_assistant
4
+
5
+ def initialize(admin_assistant)
6
+ @admin_assistant = admin_assistant
7
+ end
8
+
9
+ def actions(*a)
10
+ if a.empty?
11
+ @admin_assistant.actions
12
+ else
13
+ @admin_assistant.actions = a
14
+ end
15
+ end
16
+
17
+ def inputs
18
+ @admin_assistant.form_settings.inputs
19
+ end
20
+
21
+ def label(column, label)
22
+ @admin_assistant.custom_column_labels[column.to_s] = label
23
+ end
24
+
25
+ def form
26
+ yield @admin_assistant.form_settings
27
+ end
28
+
29
+ def index
30
+ yield @admin_assistant.index_settings
31
+ end
32
+ end
33
+
34
+ class Settings
35
+ attr_reader :column_names
36
+
37
+ def initialize(admin_assistant)
38
+ @admin_assistant = admin_assistant
39
+ end
40
+
41
+ def columns(*args)
42
+ @column_names = args
43
+ end
44
+ end
45
+
46
+ class FormSettings < Settings
47
+ attr_reader :inputs, :submit_buttons
48
+
49
+ def initialize(admin_assistant)
50
+ super
51
+ @inputs = {}
52
+ @submit_buttons = []
53
+ @read_only = []
54
+ end
55
+
56
+ def read_only(*args)
57
+ if args.empty?
58
+ @read_only
59
+ else
60
+ args.each do |arg| @read_only << arg.to_s; end
61
+ end
62
+ end
63
+ end
64
+
65
+ class IndexSettings < Settings
66
+ attr_reader :actions, :link_to_args, :search_fields, :sort_by
67
+ attr_accessor :total_entries
68
+
69
+ def initialize(admin_assistant)
70
+ super
71
+ @actions = {}
72
+ @sort_by = 'id desc'
73
+ @boolean_labels = {}
74
+ @link_to_args = {}
75
+ @search_fields = []
76
+ end
77
+
78
+ def boolean_labels(*args)
79
+ if args.size == 1
80
+ args.first.each do |column_name, pairs|
81
+ @boolean_labels[column_name.to_s] = pairs
82
+ end
83
+ else
84
+ @boolean_labels
85
+ end
86
+ end
87
+
88
+ def conditions(&block)
89
+ block ? (@conditions = block) : @conditions
90
+ end
91
+
92
+ def search(*fields)
93
+ @search_fields = fields
94
+ end
95
+
96
+ def sort_by(*sb)
97
+ if sb.empty?
98
+ @sort_by
99
+ else
100
+ @sort_by = sb
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,383 @@
1
+ class AdminAssistant
2
+ class Column
3
+ attr_accessor :custom_label
4
+
5
+ def view(action_view, opts = {})
6
+ klass = self.class.const_get 'View'
7
+ klass.new self, action_view, opts
8
+ end
9
+
10
+ class View < Delegator
11
+ attr_reader :sort_order
12
+
13
+ def initialize(column, action_view, opts)
14
+ super(column)
15
+ @column, @action_view, @opts = column, action_view, opts
16
+ @input = opts[:input]
17
+ @link_to_args = opts[:link_to_args]
18
+ @search = opts[:search]
19
+ @sort_order = opts[:sort_order]
20
+ end
21
+
22
+ def __getobj__
23
+ @column
24
+ end
25
+
26
+ def __setobj__(column)
27
+ @column = column
28
+ end
29
+
30
+ def form_value(record)
31
+ value_method = "#{@column.name}_value"
32
+ if @action_view.respond_to?(value_method)
33
+ @action_view.send value_method, record
34
+ else
35
+ field_value record
36
+ end
37
+ end
38
+
39
+ def index_header_css_class
40
+ "sort #{sort_order}" if sort_order
41
+ end
42
+
43
+ def index_td_css_class
44
+ 'sort' if sort_order
45
+ end
46
+
47
+ def index_html(record)
48
+ html_for_index_method = "#{name}_html_for_index"
49
+ html = if @action_view.respond_to?(html_for_index_method)
50
+ @action_view.send html_for_index_method, record
51
+ elsif @link_to_args
52
+ @action_view.link_to(
53
+ @action_view.send(:h, index_value(record)),
54
+ @link_to_args.call(record)
55
+ )
56
+ else
57
+ @action_view.send(:h, index_value(record))
58
+ end
59
+ html = '&nbsp;' if html.blank?
60
+ html
61
+ end
62
+
63
+ def index_value(record)
64
+ value_method = "#{@column.name}_value"
65
+ if @action_view.respond_to?(value_method)
66
+ @action_view.send value_method, record
67
+ else
68
+ field_value record
69
+ end
70
+ end
71
+
72
+ def label
73
+ if @column.custom_label
74
+ @column.custom_label
75
+ elsif @column.name.to_s == 'id'
76
+ 'ID'
77
+ else
78
+ @column.name.to_s.capitalize.gsub(/_/, ' ')
79
+ end
80
+ end
81
+
82
+ def next_sort_params
83
+ name_for_sort = name
84
+ next_sort_order = 'asc'
85
+ if sort_order
86
+ if sort_order == 'asc'
87
+ next_sort_order = 'desc'
88
+ else
89
+ name_for_sort = nil
90
+ next_sort_order = nil
91
+ end
92
+ end
93
+ {:sort => name_for_sort, :sort_order => next_sort_order}
94
+ end
95
+
96
+ def paperclip?
97
+ @column.is_a?(PaperclipColumn)
98
+ end
99
+
100
+ def sort_possible?
101
+ @column.is_a?(ActiveRecordColumn) || @column.is_a?(BelongsToColumn)
102
+ end
103
+ end
104
+ end
105
+
106
+ class ActiveRecordColumn < Column
107
+ attr_accessor :search_terms
108
+
109
+ def initialize(ar_column)
110
+ @ar_column = ar_column
111
+ end
112
+
113
+ def add_to_query(ar_query)
114
+ unless @search_terms.blank?
115
+ ar_query.boolean_join = :and
116
+ case sql_type
117
+ when :boolean
118
+ ar_query.condition_sqls << "#{name} = ?"
119
+ ar_query.bind_vars << search_value
120
+ else
121
+ ar_query.condition_sqls << "#{name} like ?"
122
+ ar_query.bind_vars << "%#{@search_terms}%"
123
+ end
124
+ end
125
+ end
126
+
127
+ def contains?(column_name)
128
+ column_name.to_s == @ar_column.name
129
+ end
130
+
131
+ def name
132
+ @ar_column.name
133
+ end
134
+
135
+ def search_value
136
+ case sql_type
137
+ when :boolean
138
+ @search_terms.blank? ? nil : (@search_terms == 'true')
139
+ else
140
+ @search_terms
141
+ end
142
+ end
143
+
144
+ def sql_type
145
+ @ar_column.type
146
+ end
147
+
148
+ class View < AdminAssistant::Column::View
149
+ def initialize(column, action_view, opts)
150
+ super
151
+ @boolean_labels = opts[:boolean_labels]
152
+ end
153
+
154
+ def add_to_form(form)
155
+ case @input || @column.sql_type
156
+ when :text
157
+ form.text_area name
158
+ when :boolean
159
+ form.check_box name
160
+ when :datetime
161
+ form.datetime_select name, :include_blank => true
162
+ when :date
163
+ form.date_select name, :include_blank => true
164
+ when :us_state
165
+ form.select(
166
+ name, ordered_us_state_names_and_codes, :include_blank => true
167
+ )
168
+ else
169
+ form.text_field name
170
+ end
171
+ end
172
+
173
+ def field_value(record)
174
+ record.send(name) if record.respond_to?(name)
175
+ end
176
+
177
+ def index_value(record)
178
+ value = super
179
+ if @boolean_labels
180
+ value = value ? @boolean_labels.first : @boolean_labels.last
181
+ end
182
+ value
183
+ end
184
+
185
+ def ordered_us_state_names_and_codes
186
+ {
187
+ 'Alabama' => 'AL', 'Alaska' => 'AK', 'Arizona' => 'AZ',
188
+ 'Arkansas' => 'AR', 'California' => 'CA', 'Colorado' => 'CO',
189
+ 'Connecticut' => 'CT', 'Delaware' => 'DE',
190
+ 'District of Columbia' => 'DC', 'Florida' => 'FL', 'Georgia' => 'GA',
191
+ 'Hawaii' => 'HI', 'Idaho' => 'ID', 'Illinois' => 'IL',
192
+ 'Indiana' => 'IN', 'Iowa' => 'IA', 'Kansas' => 'KS',
193
+ 'Kentucky' => 'KY', 'Louisiana' => 'LA', 'Maine' => 'ME',
194
+ 'Maryland' => 'MD', 'Massachusetts' => 'MA', 'Michigan' => 'MI',
195
+ 'Minnesota' => 'MN', 'Mississippi' => 'MS', 'Missouri' => 'MO',
196
+ 'Montana' => 'MT', 'Nebraska' => 'NE', 'Nevada' => 'NV',
197
+ 'New Hampshire' => 'NH', 'New Jersey' => 'NJ', 'New Mexico' => 'NM',
198
+ 'New York' => 'NY', 'North Carolina' => 'NC', 'North Dakota' => 'ND',
199
+ 'Ohio' => 'OH', 'Oklahoma' => 'OK', 'Oregon' => 'OR',
200
+ 'Pennsylvania' => 'PA', 'Puerto Rico' => 'PR',
201
+ 'Rhode Island' => 'RI', 'South Carolina' => 'SC',
202
+ 'South Dakota' => 'SD', 'Tennessee' => 'TN', 'Texas' => 'TX',
203
+ 'Utah' => 'UT', 'Vermont' => 'VT', 'Virginia' => 'VA',
204
+ 'Washington' => 'WA', 'West Virginia' => 'WV', 'Wisconsin' => 'WI',
205
+ 'Wyoming' => 'WY'
206
+ }.sort_by { |name, code| name }
207
+ end
208
+
209
+ def search_html
210
+ input = case @column.sql_type
211
+ when :boolean
212
+ opts = [['', nil]]
213
+ if @boolean_labels
214
+ opts << [@boolean_labels.first, true]
215
+ opts << [@boolean_labels.last, false]
216
+ else
217
+ opts << ['true', true]
218
+ opts << ['false', false]
219
+ end
220
+ @action_view.select("search", name, opts)
221
+ else
222
+ @action_view.text_field_tag("search[#{name}]", @search[name])
223
+ end
224
+ "<p><label>#{label}</label> <br/>#{input}</p>"
225
+ end
226
+ end
227
+ end
228
+
229
+ class AdminAssistantColumn < Column
230
+ attr_reader :name
231
+
232
+ def initialize(name)
233
+ @name = name.to_s
234
+ end
235
+
236
+ def contains?(column_name)
237
+ column_name.to_s == @name
238
+ end
239
+
240
+ class View < AdminAssistant::Column::View
241
+ def field_value(record)
242
+ nil
243
+ end
244
+ end
245
+ end
246
+
247
+ class BelongsToColumn < Column
248
+ def initialize(belongs_to_assoc)
249
+ @belongs_to_assoc = belongs_to_assoc
250
+ end
251
+
252
+ def associated_class
253
+ @belongs_to_assoc.klass
254
+ end
255
+
256
+ def association_foreign_key
257
+ @belongs_to_assoc.association_foreign_key
258
+ end
259
+
260
+ def contains?(column_name)
261
+ column_name.to_s == name
262
+ end
263
+
264
+ def default_name_method
265
+ [:name, :title, :login, :username].detect { |m|
266
+ associated_class.columns.any? { |column| column.name.to_s == m.to_s }
267
+ }
268
+ end
269
+
270
+ def name
271
+ @belongs_to_assoc.name.to_s
272
+ end
273
+
274
+ def order_sql_field
275
+ sql = "#{@belongs_to_assoc.table_name}. "
276
+ sql << if default_name_method
277
+ default_name_method.to_s
278
+ else
279
+ @belongs_to_assoc.association_foreign_key
280
+ end
281
+ end
282
+
283
+ class View < AdminAssistant::Column::View
284
+ def add_to_form(form)
285
+ form.select(
286
+ association_foreign_key,
287
+ associated_class.
288
+ find(:all).
289
+ sort_by { |model| model.send(default_name_method) }.
290
+ map { |model| [model.send(default_name_method), model.id] }
291
+ )
292
+ end
293
+
294
+ def field_value(record)
295
+ assoc_value = record.send name
296
+ if assoc_value.respond_to?(:name_for_admin_assistant)
297
+ assoc_value.name_for_admin_assistant
298
+ elsif assoc_value && default_name_method
299
+ assoc_value.send default_name_method
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ class DefaultSearchColumn < Column
306
+ attr_reader :terms
307
+
308
+ def initialize(terms, model_class)
309
+ @terms, @model_class = terms, model_class
310
+ end
311
+
312
+ def add_to_query(ar_query)
313
+ unless @terms.blank?
314
+ ar_query.boolean_join = :or
315
+ searchable_columns.each do |column|
316
+ ar_query.condition_sqls << "#{column.name} like ?"
317
+ ar_query.bind_vars << "%#{@terms}%"
318
+ end
319
+ end
320
+ end
321
+
322
+ def searchable_columns
323
+ @model_class.columns.select { |column|
324
+ [:string, :text].include?(column.type)
325
+ }
326
+ end
327
+
328
+ class View < AdminAssistant::Column::View
329
+ def search_html
330
+ @action_view.text_field_tag("search", @column.terms)
331
+ end
332
+ end
333
+ end
334
+
335
+ class FileColumnColumn < Column
336
+ attr_reader :name
337
+
338
+ def initialize(name)
339
+ @name = name.to_s
340
+ end
341
+
342
+ def contains?(column_name)
343
+ column_name.to_s == @name
344
+ end
345
+
346
+ class View < AdminAssistant::Column::View
347
+ def add_to_form(form)
348
+ form.file_field name
349
+ end
350
+
351
+ def index_html(record)
352
+ @action_view.instance_variable_set :@record, record
353
+ @action_view.image_tag(
354
+ @action_view.url_for_file_column('record', @column.name)
355
+ )
356
+ end
357
+ end
358
+ end
359
+
360
+ class PaperclipColumn < Column
361
+ attr_reader :name
362
+
363
+ def initialize(name)
364
+ @name = name.to_s
365
+ end
366
+
367
+ def contains?(column_name)
368
+ column_name.to_s == @name ||
369
+ column_name.to_s =~
370
+ /^#{@name}_(file_name|content_type|file_size|updated_at)$/
371
+ end
372
+
373
+ class View < AdminAssistant::Column::View
374
+ def add_to_form(form)
375
+ form.file_field name
376
+ end
377
+
378
+ def index_html(record)
379
+ @action_view.image_tag record.send(@column.name).url
380
+ end
381
+ end
382
+ end
383
+ end
@@ -0,0 +1,124 @@
1
+ class AdminAssistant
2
+ class FormView
3
+ def initialize(record, admin_assistant, action_view)
4
+ @record, @admin_assistant, @action_view =
5
+ record, admin_assistant, action_view
6
+ end
7
+
8
+ def action
9
+ if %w(new create).include?(controller.action_name)
10
+ 'create'
11
+ else
12
+ 'update'
13
+ end
14
+ end
15
+
16
+ def after_column_html(column)
17
+ if after = render_from_custom_template("_after_#{column.name}_input")
18
+ after
19
+ else
20
+ helper_method = "after_#{column.name}_input"
21
+ if @action_view.respond_to?(helper_method)
22
+ @action_view.send(helper_method, @record)
23
+ end
24
+ end
25
+ end
26
+
27
+ def column_html(column, rails_form)
28
+ hff = render_from_custom_template "_#{column.name}_input"
29
+ hff ||= column_html_from_helper_method(column)
30
+ hff ||= if settings.read_only.include?(column.name)
31
+ column.form_value(@record)
32
+ elsif column.respond_to?(:add_to_form)
33
+ column.add_to_form(rails_form)
34
+ else
35
+ virtual_column_html column
36
+ end
37
+ if ah = after_column_html(column)
38
+ hff << ah
39
+ end
40
+ hff
41
+ end
42
+
43
+ def column_html_from_helper_method(column)
44
+ html_method = "#{column.name}_html_for_form"
45
+ if @action_view.respond_to?(html_method)
46
+ @action_view.send(html_method, @record)
47
+ end
48
+ end
49
+
50
+ def column_names
51
+ settings.column_names ||
52
+ model_class.columns.reject { |ar_column|
53
+ %w(id created_at updated_at).include?(ar_column.name)
54
+ }.map { |ar_column|
55
+ @admin_assistant.column_name_or_assoc_name(ar_column.name)
56
+ }
57
+ end
58
+
59
+ def columns
60
+ @admin_assistant.columns(column_names).map { |c|
61
+ c.view(@action_view, :input => settings.inputs[c.name.to_sym])
62
+ }
63
+ end
64
+
65
+ def controller
66
+ @action_view.controller
67
+ end
68
+
69
+ def extra_submit_buttons
70
+ settings.submit_buttons
71
+ end
72
+
73
+ def form_for_args
74
+ args = {:url => {:action => action, :id => @record.id}}
75
+ unless @admin_assistant.paperclip_attachments.empty? &&
76
+ @admin_assistant.file_columns.empty?
77
+ args[:html] = {:multipart => true}
78
+ end
79
+ args
80
+ end
81
+
82
+ def model_class
83
+ @admin_assistant.model_class
84
+ end
85
+
86
+ def render_from_custom_template(slug)
87
+ template = File.join(
88
+ RAILS_ROOT, 'app/views', controller.controller_path, "#{slug}.html.erb"
89
+ )
90
+ if File.exist?(template)
91
+ @action_view.render(
92
+ :file => template,
93
+ :locals => {model_class.name.underscore.to_sym => @record}
94
+ )
95
+ end
96
+ end
97
+
98
+ def settings
99
+ @admin_assistant.form_settings
100
+ end
101
+
102
+ def submit_value
103
+ action.capitalize
104
+ end
105
+
106
+ def title
107
+ (@record.id ? "Edit" : "New") + " #{@admin_assistant.model_class_name}"
108
+ end
109
+
110
+ def virtual_column_html(column)
111
+ input_name = "#{model_class.name.underscore}[#{column.name}]"
112
+ input_type = settings.inputs[column.name.to_sym]
113
+ fv = column.form_value @record
114
+ if input_type
115
+ if input_type == :check_box
116
+ @action_view.send(:check_box_tag, input_name, '1', fv) +
117
+ @action_view.send(:hidden_field_tag, input_name, '0')
118
+ end
119
+ else
120
+ @action_view.send(:text_field_tag, input_name, fv)
121
+ end
122
+ end
123
+ end
124
+ end