admin_assistant 0.0.1

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