admin_assistant 0.0.1 → 1.0.0

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.
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