netzke-basepack 0.2.0.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/netzke/column.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Netzke
2
+ # TODO: rename this class, or better even, make a module out of it
2
3
  class Column
3
- def self.default_columns_for_widget(widget)
4
+ def self.default_dbfields_for_widget(widget, mode = :grid)
4
5
  raise ArgumentError, "No data_class_name specified for widget #{widget.config[:name]}" if widget.config[:data_class_name].nil?
5
6
 
6
7
  # layout = NetzkeLayout.create(:widget_name => widget.id_name, :items_class => self.name, :user_id => NetzkeLayout.user_id)
@@ -26,11 +27,19 @@ module Netzke
26
27
  res = []
27
28
  for c in columns_for_create
28
29
  # finally reverse-merge them with the defaults from the data_class
29
- res << data_class.default_column_config(c)
30
+ res << (mode == :grid ? data_class.default_column_config(c) : data_class.default_field_config(c))
30
31
  end
31
32
 
32
33
  res
33
34
  end
35
+
36
+ def self.default_columns_for_widget(widget)
37
+ default_dbfields_for_widget(widget, :grid)
38
+ end
39
+
40
+ def self.default_fields_for_widget(widget)
41
+ default_dbfields_for_widget(widget, :form)
42
+ end
34
43
 
35
44
  protected
36
45
 
@@ -0,0 +1,185 @@
1
+ module Netzke
2
+ class FormPanel < Base
3
+ interface :submit, :load
4
+
5
+ class << self
6
+ def js_base_class
7
+ "Ext.FormPanel"
8
+ end
9
+
10
+ def js_before_constructor
11
+ <<-JS
12
+ var fields = config.fields; // TODO: remove hidden fields
13
+ var recordFields = [];
14
+ var index = 0;
15
+ Ext.each(config.fields, function(field){recordFields.push({name:field.name, mapping:index++})});
16
+ var Record = Ext.data.Record.create(recordFields);
17
+ this.reader = new Ext.data.RecordArrayReader({}, Record);
18
+
19
+ JS
20
+ end
21
+
22
+ def js_default_config
23
+ super.merge({
24
+ :auto_scroll => true,
25
+ :bbar => "config.actions".l,
26
+ # :plugins => "plugins".l,
27
+ :items => "fields".l,
28
+ :default_type => 'textfield',
29
+ :body_style => 'padding:5px 5px 0',
30
+ :label_width => 150,
31
+ :listeners => {:afterlayout => {:fn => "this.afterlayoutHandler".l, :scope => this}},
32
+
33
+ #custom configs
34
+ :auto_load_data => true,
35
+ })
36
+ end
37
+
38
+
39
+ end
40
+
41
+ def self.js_extend_properties
42
+ {
43
+ :load_record => <<-JS.l,
44
+ function(id, neighbour){
45
+ var proxy = new Ext.data.HttpProxy({url:this.initialConfig.interface.load});
46
+ proxy.load({id:id, neighbour:neighbour}, this.reader, function(data){
47
+ if (data){
48
+ this.form.loadRecord(data.records[0])
49
+ }
50
+ }, this)
51
+ }
52
+ JS
53
+ :afterlayout_handler => <<-JS.l,
54
+ function() {
55
+ // Load initial data into the form
56
+ if (this.initialConfig.recordData){
57
+ var record = this.reader.readRecord(this.initialConfig.recordData);
58
+ this.form.loadRecord(record);
59
+ }
60
+ }
61
+ JS
62
+ :refresh_click => <<-JS.l,
63
+ function() {
64
+ this.loadRecord(3)
65
+ }
66
+ JS
67
+ :previous => <<-JS.l,
68
+ function() {
69
+ var currentId = this.form.getValues().id;
70
+ this.loadRecord(currentId, 'previous');
71
+ }
72
+ JS
73
+ :next => <<-JS.l,
74
+ function() {
75
+ var currentId = this.form.getValues().id;
76
+ this.loadRecord(currentId, 'next');
77
+ }
78
+ JS
79
+ :submit => <<-JS.l,
80
+ function() {
81
+ this.form.submit({
82
+ url:this.initialConfig.interface.submit
83
+ })
84
+ }
85
+ JS
86
+ }
87
+ end
88
+
89
+ # default configuration
90
+ def initial_config
91
+ {
92
+ :ext_config => {
93
+ :config_tool => false,
94
+ :border => true
95
+ },
96
+ :layout_manager => "NetzkeLayout",
97
+ :field_manager => "NetzkeFormPanelField"
98
+ }
99
+ end
100
+
101
+ def tools
102
+ [{:id => 'refresh', :on => {:click => 'refreshClick'}}]
103
+ end
104
+
105
+ def actions
106
+ [{
107
+ :text => 'Previous', :handler => 'previous'
108
+ },{
109
+ :text => 'Next', :handler => 'next'
110
+ },{
111
+ :text => 'Apply', :handler => 'submit', :disabled => !@permissions[:update] && !@permissions[:create]
112
+ }]
113
+ end
114
+
115
+ def js_config
116
+ res = super
117
+ # we pass column config at the time of instantiating the JS class
118
+ res.merge!(:fields => get_fields || config[:fields]) # first try to get columns from DB, then from config
119
+ res.merge!(:data_class_name => config[:data_class_name])
120
+ res.merge!(:record_data => config[:record].to_array(get_fields))
121
+ res
122
+ end
123
+
124
+ # get fields from layout manager
125
+ def get_fields
126
+ @fields ||=
127
+ if layout_manager_class && field_manager_class
128
+ layout = layout_manager_class.by_widget(id_name)
129
+ layout ||= field_manager_class.create_layout_for_widget(self)
130
+ layout.items_hash # TODO: bad name!
131
+ else
132
+ Netzke::Column.default_columns_for_widget(self)
133
+ end
134
+ end
135
+
136
+ def submit(params)
137
+ params.delete(:authenticity_token)
138
+ params.delete(:controller)
139
+ params.delete(:action)
140
+ book = Book.find(params[:id])
141
+ if book.nil?
142
+ book = Book.create(params)
143
+ else
144
+ book.update_attributes(params)
145
+ end
146
+ rescue ActiveRecord::UnknownAttributeError # unknown attributes get ignored
147
+ book.save
148
+ [book.to_array(get_fields)].to_json
149
+ end
150
+
151
+ def load(params)
152
+ logger.debug { "!!! params: #{params.inspect}" }
153
+ klass = config[:data_class_name].constantize
154
+ case params[:neighbour]
155
+ when "previous"
156
+ book = klass.previous(params[:id])
157
+ when "next"
158
+ book = klass.next(params[:id])
159
+ else
160
+ book = klass.find(params[:id])
161
+ end
162
+ [book && book.to_array(get_fields)].to_json
163
+ end
164
+
165
+ protected
166
+
167
+ def layout_manager_class
168
+ config[:layout_manager].constantize
169
+ rescue NameError
170
+ nil
171
+ end
172
+
173
+ def field_manager_class
174
+ config[:field_manager].constantize
175
+ rescue NameError
176
+ nil
177
+ end
178
+
179
+ def available_permissions
180
+ %w(read update create delete)
181
+ end
182
+
183
+
184
+ end
185
+ end
@@ -18,6 +18,31 @@ module Netzke
18
18
  # define connection points between client side and server side of GridPanel. See implementation of equally named methods in the GridPanelInterface module.
19
19
  interface :get_data, :post_data, :delete_data, :resize_column, :move_column, :get_cb_choices
20
20
 
21
+ module ClassMethods
22
+
23
+ # Global GridPanel configuration
24
+ def config
25
+ set_default_config({
26
+ :column_manager => "NetzkeGridPanelColumn"
27
+ })
28
+ end
29
+
30
+ def column_manager_class
31
+ config[:column_manager].constantize
32
+ rescue
33
+ nil
34
+ end
35
+ end
36
+ extend ClassMethods
37
+
38
+ def layout_manager_class
39
+ self.class.layout_manager_class
40
+ end
41
+
42
+ def column_manager_class
43
+ self.class.column_manager_class
44
+ end
45
+
21
46
  # default grid configuration
22
47
  def initial_config
23
48
  {
@@ -26,9 +51,11 @@ module Netzke
26
51
  :enable_column_filters => Netzke::Base.config[:grid_panel][:filters],
27
52
  :enable_column_move => true,
28
53
  :enable_column_resize => true,
29
- :border => true
54
+ :border => true,
55
+ :load_mask => true
30
56
  },
31
- :layout_manager => "NetzkeLayout"
57
+ :persistent_layout => true,
58
+ :persistent_config => true
32
59
  }
33
60
  end
34
61
 
@@ -36,7 +63,7 @@ module Netzke
36
63
  [{
37
64
  :name => 'columns',
38
65
  :widget_class_name => "GridPanel",
39
- :data_class_name => column_manager_class_name,
66
+ :data_class_name => column_manager_class.name,
40
67
  :ext_config => {:title => false, :config_tool => false},
41
68
  :active => true
42
69
  },{
@@ -63,25 +90,9 @@ module Netzke
63
90
  w = aggregatee_instance(:properties__general)
64
91
  w.interface_load_source(params)
65
92
  end
66
-
67
-
68
93
 
69
94
  protected
70
95
 
71
- def layout_manager_class
72
- config[:layout_manager] && config[:layout_manager].constantize
73
- end
74
-
75
- def column_manager_class_name
76
- "NetzkeGridPanelColumn"
77
- end
78
-
79
- def column_manager_class
80
- column_manager_class_name.constantize
81
- rescue NameError
82
- nil
83
- end
84
-
85
96
  def available_permissions
86
97
  %w(read update create delete)
87
98
  end
@@ -91,7 +102,7 @@ module Netzke
91
102
  # get columns from layout manager
92
103
  def get_columns
93
104
  @columns ||=
94
- if layout_manager_class && column_manager_class
105
+ if config[:persistent_layout] && layout_manager_class && column_manager_class
95
106
  layout = layout_manager_class.by_widget(id_name)
96
107
  layout ||= column_manager_class.create_layout_for_widget(self)
97
108
  layout.items_hash # TODO: bad name!
@@ -104,14 +104,18 @@ module Netzke::GridPanelInterface
104
104
  search_params = normalize_params(params)
105
105
  raise ArgumentError, "No data_class_name specified for widget '#{config[:name]}'" if !config[:data_class_name]
106
106
  records = config[:data_class_name].constantize.all(search_params.clone) # clone needed as searchlogic removes :conditions key from the hash
107
- output_array = []
108
- records.each do |r|
109
- r_array = []
110
- self.get_columns.each do |column|
111
- r_array << r.send(column[:name])
112
- end
113
- output_array << r_array
114
- end
107
+ # output_array = []
108
+ columns = get_columns
109
+ output_array = records.map{|r| r.to_array(columns)}
110
+
111
+ # records.each do |r|
112
+ # r_array = []
113
+ # self.get_columns.each do |column|
114
+ # r_array << r.send(column[:name])
115
+ # end
116
+ # output_array << r_array
117
+ # output_array << r.to_array(columns)
118
+ # end
115
119
 
116
120
  # add total_entries accessor to the result
117
121
  class << output_array
@@ -2,18 +2,18 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{netzke-basepack}
5
- s.version = "0.2.0.1"
5
+ s.version = "0.2.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Sergei Kozlov"]
9
- s.date = %q{2009-01-07}
9
+ s.date = %q{2009-01-23}
10
10
  s.description = %q{Base Netzke widgets - grid, form, tree, and more}
11
11
  s.email = %q{sergei@writelesscode.com}
12
- s.extra_rdoc_files = ["CHANGELOG", "lib/app/models/netzke_grid_panel_column.rb", "lib/netzke/accordion_panel.rb", "lib/netzke/ar_ext.rb", "lib/netzke/border_layout_panel.rb", "lib/netzke/column.rb", "lib/netzke/container.rb", "lib/netzke/grid_panel.rb", "lib/netzke/grid_panel_interface.rb", "lib/netzke/grid_panel_js_builder.rb", "lib/netzke/panel.rb", "lib/netzke/preference_grid.rb", "lib/netzke/properties_tool.rb", "lib/netzke/property_grid.rb", "lib/netzke/wrapper.rb", "lib/netzke-basepack.rb", "LICENSE", "README.mdown", "tasks/netzke_basepack_tasks.rake"]
13
- s.files = ["CHANGELOG", "css/basepack.css", "generators/netzke_basepack/netzke_basepack_generator.rb", "generators/netzke_basepack/netzke_grid_panel_generator.rb", "generators/netzke_basepack/templates/create_netzke_grid_panel_columns.rb", "generators/netzke_basepack/USAGE", "init.rb", "install.rb", "javascripts/basepack.js", "javascripts/filters.js", "lib/app/models/netzke_grid_panel_column.rb", "lib/netzke/accordion_panel.rb", "lib/netzke/ar_ext.rb", "lib/netzke/border_layout_panel.rb", "lib/netzke/column.rb", "lib/netzke/container.rb", "lib/netzke/grid_panel.rb", "lib/netzke/grid_panel_interface.rb", "lib/netzke/grid_panel_js_builder.rb", "lib/netzke/panel.rb", "lib/netzke/preference_grid.rb", "lib/netzke/properties_tool.rb", "lib/netzke/property_grid.rb", "lib/netzke/wrapper.rb", "lib/netzke-basepack.rb", "LICENSE", "Manifest", "Rakefile", "README.mdown", "tasks/netzke_basepack_tasks.rake", "test/app_root/app/controllers/application.rb", "test/app_root/app/models/book.rb", "test/app_root/app/models/category.rb", "test/app_root/app/models/city.rb", "test/app_root/app/models/continent.rb", "test/app_root/app/models/country.rb", "test/app_root/app/models/genre.rb", "test/app_root/config/boot.rb", "test/app_root/config/database.yml", "test/app_root/config/environment.rb", "test/app_root/config/environments/in_memory.rb", "test/app_root/config/environments/mysql.rb", "test/app_root/config/environments/postgresql.rb", "test/app_root/config/environments/sqlite.rb", "test/app_root/config/environments/sqlite3.rb", "test/app_root/config/routes.rb", "test/app_root/db/migrate/20081222033343_create_books.rb", "test/app_root/db/migrate/20081222033440_create_genres.rb", "test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb", "test/app_root/db/migrate/20081223024935_create_categories.rb", "test/app_root/db/migrate/20081223025635_create_countries.rb", "test/app_root/db/migrate/20081223025653_create_continents.rb", "test/app_root/db/migrate/20081223025732_create_cities.rb", "test/app_root/script/console", "test/ar_ext_test.rb", "test/border_layout_panel_test.rb", "test/column_test.rb", "test/console_with_fixtures.rb", "test/fixtures/books.yml", "test/fixtures/categories.yml", "test/fixtures/cities.yml", "test/fixtures/continents.yml", "test/fixtures/countries.yml", "test/fixtures/genres.yml", "test/grid_panel_test.rb", "test/netzke_basepack_test.rb", "test/schema.rb", "test/test_helper.rb", "uninstall.rb", "netzke-basepack.gemspec"]
12
+ s.extra_rdoc_files = ["CHANGELOG", "lib/app/models/netzke_form_panel_field.rb", "lib/app/models/netzke_grid_panel_column.rb", "lib/netzke/accordion_panel.rb", "lib/netzke/ar_ext.rb", "lib/netzke/border_layout_panel.rb", "lib/netzke/column.rb", "lib/netzke/container.rb", "lib/netzke/form_panel.rb", "lib/netzke/grid_panel.rb", "lib/netzke/grid_panel_interface.rb", "lib/netzke/grid_panel_js_builder.rb", "lib/netzke/panel.rb", "lib/netzke/preference_grid.rb", "lib/netzke/properties_tool.rb", "lib/netzke/property_grid.rb", "lib/netzke/wrapper.rb", "lib/netzke-basepack.rb", "LICENSE", "README.rdoc", "tasks/netzke_basepack_tasks.rake"]
13
+ s.files = ["CHANGELOG", "css/basepack.css", "generators/netzke_basepack/netzke_basepack_generator.rb", "generators/netzke_basepack/USAGE", "generators/netzke_form_panel/netzke_form_panel_generator.rb", "generators/netzke_form_panel/templates/create_netzke_form_panel_fields.rb", "generators/netzke_grid_panel/netzke_grid_panel_generator.rb", "generators/netzke_grid_panel/templates/create_netzke_grid_panel_columns.rb", "init.rb", "install.rb", "javascripts/basepack.js", "javascripts/filters.js", "lib/app/models/netzke_form_panel_field.rb", "lib/app/models/netzke_grid_panel_column.rb", "lib/netzke/accordion_panel.rb", "lib/netzke/ar_ext.rb", "lib/netzke/border_layout_panel.rb", "lib/netzke/column.rb", "lib/netzke/container.rb", "lib/netzke/form_panel.rb", "lib/netzke/grid_panel.rb", "lib/netzke/grid_panel_interface.rb", "lib/netzke/grid_panel_js_builder.rb", "lib/netzke/panel.rb", "lib/netzke/preference_grid.rb", "lib/netzke/properties_tool.rb", "lib/netzke/property_grid.rb", "lib/netzke/wrapper.rb", "lib/netzke-basepack.rb", "LICENSE", "Manifest", "Rakefile", "README.rdoc", "tasks/netzke_basepack_tasks.rake", "test/app_root/app/controllers/application.rb", "test/app_root/app/models/book.rb", "test/app_root/app/models/category.rb", "test/app_root/app/models/city.rb", "test/app_root/app/models/continent.rb", "test/app_root/app/models/country.rb", "test/app_root/app/models/genre.rb", "test/app_root/config/boot.rb", "test/app_root/config/database.yml", "test/app_root/config/environment.rb", "test/app_root/config/environments/in_memory.rb", "test/app_root/config/environments/mysql.rb", "test/app_root/config/environments/postgresql.rb", "test/app_root/config/environments/sqlite.rb", "test/app_root/config/environments/sqlite3.rb", "test/app_root/config/routes.rb", "test/app_root/db/migrate/20081222033343_create_books.rb", "test/app_root/db/migrate/20081222033440_create_genres.rb", "test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb", "test/app_root/db/migrate/20081223024935_create_categories.rb", "test/app_root/db/migrate/20081223025635_create_countries.rb", "test/app_root/db/migrate/20081223025653_create_continents.rb", "test/app_root/db/migrate/20081223025732_create_cities.rb", "test/app_root/db/migrate/20090102223630_create_netzke_layouts.rb", "test/app_root/db/migrate/20090102223811_create_netzke_grid_panel_columns.rb", "test/app_root/script/console", "test/app_root/vendor/plugins/acts_as_list/init.rb", "test/app_root/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb", "test/app_root/vendor/plugins/acts_as_list/README", "test/ar_ext_test.rb", "test/border_layout_panel_test.rb", "test/column_test.rb", "test/console_with_fixtures.rb", "test/fixtures/books.yml", "test/fixtures/categories.yml", "test/fixtures/cities.yml", "test/fixtures/continents.yml", "test/fixtures/countries.yml", "test/fixtures/genres.yml", "test/grid_panel_test.rb", "test/netzke_basepack_test.rb", "test/schema.rb", "test/test_helper.rb", "uninstall.rb", "netzke-basepack.gemspec"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{http://writelesscode.com}
16
- s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Netzke-basepack", "--main", "README.mdown"]
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Netzke-basepack", "--main", "README.rdoc"]
17
17
  s.require_paths = ["lib"]
18
18
  s.rubyforge_project = %q{netzke-basepack}
19
19
  s.rubygems_version = %q{1.3.1}
@@ -26,13 +26,13 @@ Gem::Specification.new do |s|
26
26
 
27
27
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
28
  s.add_runtime_dependency(%q<searchlogic>, [">= 1.6.2"])
29
- s.add_runtime_dependency(%q<netzke-core>, [">= 0", "= 0.2.1"])
29
+ s.add_runtime_dependency(%q<netzke-core>, [">= 0", "= 0.2.2"])
30
30
  else
31
31
  s.add_dependency(%q<searchlogic>, [">= 1.6.2"])
32
- s.add_dependency(%q<netzke-core>, [">= 0", "= 0.2.1"])
32
+ s.add_dependency(%q<netzke-core>, [">= 0", "= 0.2.2"])
33
33
  end
34
34
  else
35
35
  s.add_dependency(%q<searchlogic>, [">= 1.6.2"])
36
- s.add_dependency(%q<netzke-core>, [">= 0", "= 0.2.1"])
36
+ s.add_dependency(%q<netzke-core>, [">= 0", "= 0.2.2"])
37
37
  end
38
38
  end
@@ -0,0 +1,14 @@
1
+ class CreateNetzkeLayouts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :netzke_layouts do |t|
4
+ t.string :widget_name
5
+ t.string :items_class
6
+ t.integer :user_id
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :netzke_layouts
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class CreateNetzkeGridPanelColumns < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :netzke_grid_panel_columns do |t|
4
+ t.string :name
5
+ t.string :label
6
+ t.boolean :read_only
7
+ t.integer :position
8
+ t.boolean :hidden
9
+ t.integer :width
10
+ t.string :editor, :limit => 32
11
+
12
+ t.integer :layout_id
13
+
14
+ t.timestamps
15
+ end
16
+ end
17
+
18
+ def self.down
19
+ drop_table :netzke_grid_panel_columns
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ ActsAsList
2
+ ==========
3
+
4
+ This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
5
+
6
+
7
+ Example
8
+ =======
9
+
10
+ class TodoList < ActiveRecord::Base
11
+ has_many :todo_items, :order => "position"
12
+ end
13
+
14
+ class TodoItem < ActiveRecord::Base
15
+ belongs_to :todo_list
16
+ acts_as_list :scope => :todo_list
17
+ end
18
+
19
+ todo_list.first.move_to_bottom
20
+ todo_list.last.move_higher
21
+
22
+
23
+ Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
@@ -0,0 +1,3 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}/lib"
2
+ require 'active_record/acts/list'
3
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
@@ -0,0 +1,256 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module List #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ def acts_as_list(options = {})
34
+ configuration = { :column => "position", :scope => "1 = 1" }
35
+ configuration.update(options) if options.is_a?(Hash)
36
+
37
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38
+
39
+ if configuration[:scope].is_a?(Symbol)
40
+ scope_condition_method = %(
41
+ def scope_condition
42
+ if #{configuration[:scope].to_s}.nil?
43
+ "#{configuration[:scope].to_s} IS NULL"
44
+ else
45
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46
+ end
47
+ end
48
+ )
49
+ else
50
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51
+ end
52
+
53
+ class_eval <<-EOV
54
+ include ActiveRecord::Acts::List::InstanceMethods
55
+
56
+ def acts_as_list_class
57
+ ::#{self.name}
58
+ end
59
+
60
+ def position_column
61
+ '#{configuration[:column]}'
62
+ end
63
+
64
+ #{scope_condition_method}
65
+
66
+ before_destroy :remove_from_list
67
+ before_create :add_to_list_bottom
68
+ EOV
69
+ end
70
+ end
71
+
72
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75
+ # the first in the list of all chapters.
76
+ module InstanceMethods
77
+ # Insert the item at the given position (defaults to the top position of 1).
78
+ def insert_at(position = 1)
79
+ insert_at_position(position)
80
+ end
81
+
82
+ # Swap positions with the next lower item, if one exists.
83
+ def move_lower
84
+ return unless lower_item
85
+
86
+ acts_as_list_class.transaction do
87
+ lower_item.decrement_position
88
+ increment_position
89
+ end
90
+ end
91
+
92
+ # Swap positions with the next higher item, if one exists.
93
+ def move_higher
94
+ return unless higher_item
95
+
96
+ acts_as_list_class.transaction do
97
+ higher_item.increment_position
98
+ decrement_position
99
+ end
100
+ end
101
+
102
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
103
+ # position adjusted accordingly.
104
+ def move_to_bottom
105
+ return unless in_list?
106
+ acts_as_list_class.transaction do
107
+ decrement_positions_on_lower_items
108
+ assume_bottom_position
109
+ end
110
+ end
111
+
112
+ # Move to the top of the list. If the item is already in the list, the items above it have their
113
+ # position adjusted accordingly.
114
+ def move_to_top
115
+ return unless in_list?
116
+ acts_as_list_class.transaction do
117
+ increment_positions_on_higher_items
118
+ assume_top_position
119
+ end
120
+ end
121
+
122
+ # Removes the item from the list.
123
+ def remove_from_list
124
+ if in_list?
125
+ decrement_positions_on_lower_items
126
+ update_attribute position_column, nil
127
+ end
128
+ end
129
+
130
+ # Increase the position of this item without adjusting the rest of the list.
131
+ def increment_position
132
+ return unless in_list?
133
+ update_attribute position_column, self.send(position_column).to_i + 1
134
+ end
135
+
136
+ # Decrease the position of this item without adjusting the rest of the list.
137
+ def decrement_position
138
+ return unless in_list?
139
+ update_attribute position_column, self.send(position_column).to_i - 1
140
+ end
141
+
142
+ # Return +true+ if this object is the first in the list.
143
+ def first?
144
+ return false unless in_list?
145
+ self.send(position_column) == 1
146
+ end
147
+
148
+ # Return +true+ if this object is the last in the list.
149
+ def last?
150
+ return false unless in_list?
151
+ self.send(position_column) == bottom_position_in_list
152
+ end
153
+
154
+ # Return the next higher item in the list.
155
+ def higher_item
156
+ return nil unless in_list?
157
+ acts_as_list_class.find(:first, :conditions =>
158
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159
+ )
160
+ end
161
+
162
+ # Return the next lower item in the list.
163
+ def lower_item
164
+ return nil unless in_list?
165
+ acts_as_list_class.find(:first, :conditions =>
166
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167
+ )
168
+ end
169
+
170
+ # Test if this record is in a list
171
+ def in_list?
172
+ !send(position_column).nil?
173
+ end
174
+
175
+ private
176
+ def add_to_list_top
177
+ increment_positions_on_all_items
178
+ end
179
+
180
+ def add_to_list_bottom
181
+ self[position_column] = bottom_position_in_list.to_i + 1
182
+ end
183
+
184
+ # Overwrite this method to define the scope of the list changes
185
+ def scope_condition() "1" end
186
+
187
+ # Returns the bottom position number in the list.
188
+ # bottom_position_in_list # => 2
189
+ def bottom_position_in_list(except = nil)
190
+ item = bottom_item(except)
191
+ item ? item.send(position_column) : 0
192
+ end
193
+
194
+ # Returns the bottom item
195
+ def bottom_item(except = nil)
196
+ conditions = scope_condition
197
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199
+ end
200
+
201
+ # Forces item to assume the bottom position in the list.
202
+ def assume_bottom_position
203
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204
+ end
205
+
206
+ # Forces item to assume the top position in the list.
207
+ def assume_top_position
208
+ update_attribute(position_column, 1)
209
+ end
210
+
211
+ # This has the effect of moving all the higher items up one.
212
+ def decrement_positions_on_higher_items(position)
213
+ acts_as_list_class.update_all(
214
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215
+ )
216
+ end
217
+
218
+ # This has the effect of moving all the lower items up one.
219
+ def decrement_positions_on_lower_items
220
+ return unless in_list?
221
+ acts_as_list_class.update_all(
222
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223
+ )
224
+ end
225
+
226
+ # This has the effect of moving all the higher items down one.
227
+ def increment_positions_on_higher_items
228
+ return unless in_list?
229
+ acts_as_list_class.update_all(
230
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231
+ )
232
+ end
233
+
234
+ # This has the effect of moving all the lower items down one.
235
+ def increment_positions_on_lower_items(position)
236
+ acts_as_list_class.update_all(
237
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238
+ )
239
+ end
240
+
241
+ # Increments position (<tt>position_column</tt>) of all items in the list.
242
+ def increment_positions_on_all_items
243
+ acts_as_list_class.update_all(
244
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
245
+ )
246
+ end
247
+
248
+ def insert_at_position(position)
249
+ remove_from_list
250
+ increment_positions_on_lower_items(position)
251
+ self.update_attribute(position_column, position)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end