admin_assistant 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/README +3 -28
  2. data/Rakefile +18 -10
  3. data/init.rb +1 -0
  4. data/install.rb +5 -1
  5. data/lib/admin_assistant/active_record_column.rb +211 -0
  6. data/lib/admin_assistant/association_target.rb +35 -0
  7. data/lib/admin_assistant/belongs_to_column.rb +186 -0
  8. data/lib/admin_assistant/builder.rb +222 -35
  9. data/lib/admin_assistant/column.rb +266 -297
  10. data/lib/admin_assistant/default_search_column.rb +41 -0
  11. data/lib/admin_assistant/file_column_column.rb +73 -0
  12. data/lib/admin_assistant/form_view.rb +7 -68
  13. data/lib/admin_assistant/helper.rb +4 -2
  14. data/lib/admin_assistant/index.rb +101 -81
  15. data/lib/admin_assistant/paperclip_column.rb +45 -0
  16. data/lib/admin_assistant/polymorphic_belongs_to_column.rb +102 -0
  17. data/lib/admin_assistant/request/autocomplete.rb +47 -0
  18. data/lib/admin_assistant/request/base.rb +176 -0
  19. data/lib/admin_assistant/request/create.rb +27 -0
  20. data/lib/admin_assistant/request/destroy.rb +15 -0
  21. data/lib/admin_assistant/request/edit.rb +11 -0
  22. data/lib/admin_assistant/request/index.rb +26 -0
  23. data/lib/admin_assistant/request/new.rb +19 -0
  24. data/lib/admin_assistant/request/show.rb +24 -0
  25. data/lib/admin_assistant/request/update.rb +44 -0
  26. data/lib/admin_assistant/search.rb +82 -0
  27. data/lib/admin_assistant/show_view.rb +20 -0
  28. data/lib/admin_assistant/virtual_column.rb +61 -0
  29. data/lib/admin_assistant.rb +190 -85
  30. data/lib/javascripts/admin_assistant.js +253 -0
  31. data/lib/stylesheets/activescaffold.css +219 -0
  32. data/lib/stylesheets/default.css +119 -0
  33. data/lib/views/_polymorphic_field_search.html.erb +89 -0
  34. data/lib/views/_restricted_autocompleter.html.erb +53 -0
  35. data/lib/views/autocomplete.html.erb +11 -0
  36. data/lib/views/form.html.erb +6 -3
  37. data/lib/views/index.html.erb +53 -46
  38. data/lib/views/show.html.erb +19 -0
  39. data/vendor/ar_query/MIT-LICENSE +20 -0
  40. data/vendor/ar_query/README +0 -0
  41. data/vendor/ar_query/init.rb +1 -0
  42. data/vendor/ar_query/install.rb +1 -0
  43. data/vendor/ar_query/lib/ar_query.rb +137 -0
  44. data/vendor/ar_query/spec/ar_query_spec.rb +253 -0
  45. data/vendor/ar_query/tasks/ar_query_tasks.rake +0 -0
  46. data/vendor/ar_query/uninstall.rb +1 -0
  47. metadata +39 -16
  48. data/lib/admin_assistant/request.rb +0 -183
  49. data/lib/stylesheets/admin_assistant.css +0 -75
data/README CHANGED
@@ -1,6 +1,9 @@
1
1
  admin_assistant
2
2
  ===============
3
3
 
4
+ Documentation: http://fhwang.github.com/admin_assistant/
5
+ Google Group: http://groups.google.com/group/admin_assistant
6
+
4
7
  admin_assistant is a Rails plugin that automates a lot of features typically
5
8
  needed in admin interfaces. Current features include:
6
9
 
@@ -12,32 +15,4 @@ needed in admin interfaces. Current features include:
12
15
  * Simple handling of belongs_to association via drop-down selects
13
16
  * Built-in support for Paperclip and FileColumn
14
17
 
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
18
  Copyright (c) 2009 Francis Hwang, released under the MIT license
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require 'grancher/task'
1
2
  require 'rake'
2
3
  require 'rake/testtask'
3
4
  require 'rake/rdoctask'
@@ -6,16 +7,12 @@ require 'spec/rake/spectask'
6
7
  desc 'Default: run all specs across all supported Rails gem versions.'
7
8
  task :default => :spec
8
9
 
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
10
+ # run with rake publish
11
+ Grancher::Task.new do |g|
12
+ g.branch = 'gh-pages'
13
+ g.push_to = 'origin' # automatically push too
14
+
15
+ g.directory 'website'
19
16
  end
20
17
 
21
18
  desc 'Generate documentation for the admin_assistant plugin.'
@@ -26,3 +23,14 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
26
23
  rdoc.rdoc_files.include('README')
27
24
  rdoc.rdoc_files.include('lib/**/*.rb')
28
25
  end
26
+
27
+ desc 'Run all specs across all supported Rails gem versions.'
28
+ task :spec do
29
+ versions = %w(2.1.0 2.1.2 2.2.2 2.3.2 2.3.3)
30
+ cmd = "cd test_rails_app && " + (versions.map { |version|
31
+ "echo '===== Testing #{version} =====' && RAILS_GEM_VERSION=#{version} rake"
32
+ }.join(" && "))
33
+ puts cmd
34
+ puts `#{cmd}`
35
+ end
36
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "#{File.dirname(__FILE__)}/lib/admin_assistant"
data/install.rb CHANGED
@@ -1 +1,5 @@
1
- # Install hook code here
1
+ # Removing directories that would be spammy if you were just using this as a
2
+ # plugin
3
+ FileUtils.rm_rf "#{File.dirname(__FILE__)}/doc"
4
+ FileUtils.rm_rf "#{File.dirname(__FILE__)}/test_rails_app"
5
+
@@ -0,0 +1,211 @@
1
+ class AdminAssistant
2
+ class ActiveRecordColumn < Column
3
+ def initialize(ar_column)
4
+ @ar_column = ar_column
5
+ end
6
+
7
+ def add_to_query_condition(ar_query_condition, search)
8
+ table_name = search.model_class.table_name
9
+ if blank?(search)
10
+ ar_query_condition.add_condition do |sub_cond|
11
+ sub_cond.boolean_join = :or
12
+ sub_cond.sqls << "#{table_name}.#{name} is null"
13
+ sub_cond.sqls << "#{table_name}.#{name} = ''"
14
+ end
15
+ else
16
+ value_for_query = search.send(@ar_column.name)
17
+ unless value_for_query.nil?
18
+ comp = comparator(search)
19
+ unless %w(< <= = >= >).include?(comp)
20
+ comp = nil
21
+ end
22
+ if comp
23
+ ar_query_condition.sqls << "#{table_name}.#{name} #{comp} ?"
24
+ ar_query_condition.bind_vars << value_for_query
25
+ else
26
+ case field_type
27
+ when :boolean, :integer
28
+ ar_query_condition.sqls << "#{table_name}.#{name} = ?"
29
+ ar_query_condition.bind_vars << value_for_query
30
+ else
31
+ ar_query_condition.sqls << "LOWER(#{table_name}.#{name}) like LOWER(?)"
32
+ ar_query_condition.bind_vars << "%#{value_for_query}%"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def attributes_for_search_object(search_params)
40
+ terms = search_params[@ar_column.name]
41
+ value = unless terms.blank?
42
+ case field_type
43
+ when :boolean
44
+ terms.blank? ? nil : (terms == 'true')
45
+ else
46
+ terms
47
+ end
48
+ end
49
+ {name => value}
50
+ end
51
+
52
+ def contains?(column_name)
53
+ column_name.to_s == @ar_column.name
54
+ end
55
+
56
+ def field_type
57
+ @ar_column.type
58
+ end
59
+
60
+ def name
61
+ @ar_column.name
62
+ end
63
+
64
+ class FormView < AdminAssistant::Column::View
65
+ include AdminAssistant::Column::FormViewMethods
66
+
67
+ def check_box_html(form)
68
+ form.check_box name
69
+ end
70
+
71
+ def date_select_html(form)
72
+ form.date_select(
73
+ name, {:include_blank => true}.merge(@date_select_options)
74
+ )
75
+ end
76
+
77
+ def datetime_select_html(form)
78
+ opts = {:include_blank => true}.merge @datetime_select_options
79
+ h = form.datetime_select name, opts
80
+ if opts[:include_blank]
81
+ js_name = "#{form.object.class.name.underscore}_#{name}"
82
+ name = @clear_link || "Clear"
83
+ h << @action_view.send(
84
+ :link_to_function, name,
85
+ "AdminAssistant.clear_datetime_select('#{js_name}')"
86
+ )
87
+ end
88
+ h
89
+ end
90
+
91
+ def default_html(form)
92
+ input = @input || default_input
93
+ self.send("#{input}_html", form)
94
+ end
95
+
96
+ def default_input
97
+ case @column.field_type
98
+ when :boolean
99
+ :check_box
100
+ when :date
101
+ :date_select
102
+ when :datetime
103
+ :datetime_select
104
+ when :text
105
+ :text_area
106
+ else
107
+ :text_field
108
+ end
109
+ end
110
+
111
+ def ordered_us_state_names_and_codes
112
+ {
113
+ 'Alabama' => 'AL', 'Alaska' => 'AK', 'Arizona' => 'AZ',
114
+ 'Arkansas' => 'AR', 'California' => 'CA', 'Colorado' => 'CO',
115
+ 'Connecticut' => 'CT', 'Delaware' => 'DE',
116
+ 'District of Columbia' => 'DC', 'Florida' => 'FL', 'Georgia' => 'GA',
117
+ 'Hawaii' => 'HI', 'Idaho' => 'ID', 'Illinois' => 'IL',
118
+ 'Indiana' => 'IN', 'Iowa' => 'IA', 'Kansas' => 'KS',
119
+ 'Kentucky' => 'KY', 'Louisiana' => 'LA', 'Maine' => 'ME',
120
+ 'Maryland' => 'MD', 'Massachusetts' => 'MA', 'Michigan' => 'MI',
121
+ 'Minnesota' => 'MN', 'Mississippi' => 'MS', 'Missouri' => 'MO',
122
+ 'Montana' => 'MT', 'Nebraska' => 'NE', 'Nevada' => 'NV',
123
+ 'New Hampshire' => 'NH', 'New Jersey' => 'NJ', 'New Mexico' => 'NM',
124
+ 'New York' => 'NY', 'North Carolina' => 'NC', 'North Dakota' => 'ND',
125
+ 'Ohio' => 'OH', 'Oklahoma' => 'OK', 'Oregon' => 'OR',
126
+ 'Pennsylvania' => 'PA', 'Puerto Rico' => 'PR',
127
+ 'Rhode Island' => 'RI', 'South Carolina' => 'SC',
128
+ 'South Dakota' => 'SD', 'Tennessee' => 'TN', 'Texas' => 'TX',
129
+ 'Utah' => 'UT', 'Vermont' => 'VT', 'Virginia' => 'VA',
130
+ 'Washington' => 'WA', 'West Virginia' => 'WV', 'Wisconsin' => 'WI',
131
+ 'Wyoming' => 'WY'
132
+ }.sort_by { |name, code| name }
133
+ end
134
+
135
+ def select_html(form)
136
+ # for now only used for boolean fields
137
+ value = form.object.send name
138
+ selected = if value
139
+ '1'
140
+ elsif value == false
141
+ '0'
142
+ end
143
+ form.select(
144
+ name, [[true, '1'], [false, '0']],
145
+ @select_options.merge(:selected => selected)
146
+ )
147
+ end
148
+
149
+ def text_area_html(form)
150
+ form.text_area name, @text_area_options
151
+ end
152
+
153
+ def text_field_html(form)
154
+ form.text_field name
155
+ end
156
+
157
+ def us_state_html(form)
158
+ form.select(
159
+ name, ordered_us_state_names_and_codes, :include_blank => true
160
+ )
161
+ end
162
+ end
163
+
164
+ class IndexView < AdminAssistant::Column::View
165
+ include AdminAssistant::Column::IndexViewMethods
166
+
167
+ def ajax_toggle?
168
+ @column.field_type == :boolean && @ajax_toggle_allowed
169
+ end
170
+
171
+ def ajax_toggle_div_id(record)
172
+ "#{record.class.name.underscore}_#{record.id}_#{name}"
173
+ end
174
+
175
+ def ajax_toggle_html(record)
176
+ <<-HTML
177
+ <div id="#{ ajax_toggle_div_id(record) }">
178
+ #{ajax_toggle_inner_html(record)}
179
+ </div>
180
+ HTML
181
+ end
182
+
183
+ def ajax_toggle_inner_html(record)
184
+ div_id = ajax_toggle_div_id record
185
+ @action_view.link_to_remote(
186
+ string(record),
187
+ :update => div_id,
188
+ :url => {
189
+ :action => 'update', :id => record.id, :from => div_id,
190
+ record.class.name.underscore.to_sym => {
191
+ name => (!value(record) ? '1' : '0')
192
+ }
193
+ },
194
+ :success => "$(#{div_id}).hide(); $(#{div_id}).appear()"
195
+ )
196
+ end
197
+
198
+ def unconfigured_html(record)
199
+ if ajax_toggle?
200
+ ajax_toggle_html(record)
201
+ else
202
+ @action_view.send(:h, string(record))
203
+ end
204
+ end
205
+ end
206
+
207
+ class SearchView < AdminAssistant::Column::View
208
+ include AdminAssistant::Column::SimpleColumnSearchViewMethods
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,35 @@
1
+ class AdminAssistant
2
+ class AssociationTarget
3
+ def initialize(associated_class)
4
+ @associated_class = associated_class
5
+ end
6
+
7
+ def assoc_value(assoc_value)
8
+ if assoc_value.respond_to?(:name_for_admin_assistant)
9
+ assoc_value.name_for_admin_assistant
10
+ elsif assoc_value && default_name_method
11
+ assoc_value.send default_name_method
12
+ end
13
+ end
14
+
15
+ def default_name_method
16
+ [:name, :title, :login, :username].detect { |m|
17
+ @associated_class.columns.any? { |column| column.name.to_s == m.to_s }
18
+ }
19
+ end
20
+
21
+ def name
22
+ @associated_class.name.gsub(/([A-Z])/, ' \1')[1..-1].downcase
23
+ end
24
+
25
+ def options_for_select
26
+ records = @associated_class.find(:all)
27
+ if default_name_method
28
+ records.sort_by { |model| model.send(default_name_method) }.
29
+ map { |model| [model.send(default_name_method), model.id] }
30
+ else
31
+ records.map &:id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,186 @@
1
+ class AdminAssistant
2
+ class BelongsToColumn < Column
3
+ attr_reader :match_text_fields_in_search
4
+
5
+ def initialize(belongs_to_assoc, opts)
6
+ @belongs_to_assoc = belongs_to_assoc
7
+ @match_text_fields_in_search = opts[:match_text_fields_in_search]
8
+ @sort_by = opts[:sort_by]
9
+ @association_target = AssociationTarget.new associated_class
10
+ end
11
+
12
+ def add_to_query_condition(ar_query_condition, search)
13
+ if @match_text_fields_in_search
14
+ add_to_query_condition_by_matching_text_fields(
15
+ ar_query_condition, search
16
+ )
17
+ elsif value = search.send(association_foreign_key)
18
+ ar_query_condition.sqls << "#{association_foreign_key} = ?"
19
+ ar_query_condition.bind_vars << value
20
+ end
21
+ end
22
+
23
+ def add_to_query_condition_by_matching_text_fields(
24
+ ar_query_condition, search
25
+ )
26
+ if (value = search.send(name)) and !value.blank?
27
+ ar_query_condition.ar_query.joins << name.to_sym
28
+ searchable_columns = Model.new(associated_class).searchable_columns
29
+ ar_query_condition.add_condition do |sub_cond|
30
+ sub_cond.boolean_join = :or
31
+ searchable_columns.each do |column|
32
+ sub_cond.sqls <<
33
+ "LOWER(#{associated_class.table_name}.#{column.name}) like LOWER(?)"
34
+ sub_cond.bind_vars << "%#{value}%"
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def associated_class
41
+ @belongs_to_assoc.klass
42
+ end
43
+
44
+ def association_foreign_key
45
+ @belongs_to_assoc.options[:foreign_key] ||
46
+ @belongs_to_assoc.association_foreign_key
47
+ end
48
+
49
+ def attributes_for_search_object(search_params)
50
+ atts = {}
51
+ if @match_text_fields_in_search
52
+ atts[name.to_sym] = search_params[name]
53
+ else
54
+ terms = search_params[association_foreign_key]
55
+ associated_id = terms.to_i unless terms.blank?
56
+ atts[association_foreign_key.to_sym] = associated_id
57
+ atts[name.to_sym] = if associated_id
58
+ associated_class.find associated_id
59
+ end
60
+ end
61
+ atts
62
+ end
63
+
64
+ def contains?(column_name)
65
+ column_name.to_s == name
66
+ end
67
+
68
+ def default_name_method
69
+ @association_target.default_name_method
70
+ end
71
+
72
+ def name
73
+ @belongs_to_assoc.name.to_s
74
+ end
75
+
76
+ def order_sql_field
77
+ if @sort_by
78
+ "#{@belongs_to_assoc.table_name}.#{@sort_by}"
79
+ elsif default_name_method
80
+ "#{@belongs_to_assoc.table_name}.#{default_name_method.to_s}"
81
+ else
82
+ "#{@belongs_to_assoc.active_record.table_name}.#{@belongs_to_assoc.association_foreign_key}"
83
+ end
84
+ end
85
+
86
+ def value_for_search_object(search_params)
87
+ if @match_text_fields_in_search
88
+ search_params[name]
89
+ else
90
+ terms = search_params[association_foreign_key]
91
+ associated_id = terms.to_i unless terms.blank?
92
+ if associated_id
93
+ associated_class.find(associated_id)
94
+ end
95
+ end
96
+ end
97
+
98
+ class View < AdminAssistant::Column::View
99
+ def initialize(column, action_view, admin_assistant, opts = {})
100
+ super
101
+ @association_target = AssociationTarget.new associated_class
102
+ end
103
+
104
+ def assoc_value(assoc_value)
105
+ @association_target.assoc_value assoc_value
106
+ end
107
+
108
+ def associated_class
109
+ @column.associated_class
110
+ end
111
+
112
+ def association_foreign_key
113
+ @column.association_foreign_key
114
+ end
115
+
116
+ def value(record)
117
+ assoc_value record.send(name)
118
+ end
119
+
120
+ def options_for_select
121
+ @association_target.options_for_select
122
+ end
123
+ end
124
+
125
+ class FormView < View
126
+ include AdminAssistant::Column::FormViewMethods
127
+
128
+ def default_html(form)
129
+ if associated_class.count > 15
130
+ @action_view.send(
131
+ :render,
132
+ :file => AdminAssistant.template_file('_restricted_autocompleter'),
133
+ :use_full_path => false,
134
+ :locals => {
135
+ :form => form, :column => @column,
136
+ :select_options => @select_options
137
+ }
138
+ )
139
+ else
140
+ form.select(
141
+ association_foreign_key, options_for_select, @select_options
142
+ )
143
+ end
144
+ end
145
+ end
146
+
147
+ class IndexView < View
148
+ include AdminAssistant::Column::IndexViewMethods
149
+ end
150
+
151
+ class SearchView < View
152
+ include AdminAssistant::Column::SearchViewMethods
153
+
154
+ def html(form)
155
+ input = if @column.match_text_fields_in_search
156
+ form.text_field(name)
157
+ elsif associated_class.count > 15
158
+ render_autocompleter form
159
+ else
160
+ form.select(
161
+ association_foreign_key, options_for_select,
162
+ :include_blank => true
163
+ )
164
+ end
165
+ "<p><label>#{label}</label> <br/>#{input}</p>"
166
+ end
167
+
168
+ def render_autocompleter(form)
169
+ @action_view.send(
170
+ :render,
171
+ :file => AdminAssistant.template_file('_restricted_autocompleter'),
172
+ :use_full_path => false,
173
+ :locals => {
174
+ :form => form, :column => @column,
175
+ :select_options => {:include_blank => true},
176
+ :palette_clones_input_width => false
177
+ }
178
+ )
179
+ end
180
+ end
181
+
182
+ class ShowView < View
183
+ include AdminAssistant::Column::ShowViewMethods
184
+ end
185
+ end
186
+ end