vidl-toolbox 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require File.dirname(__FILE__) << "/lib/toolbox/version"
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.name = %q{toolbox}
6
+ s.version = Toolbox::VERSION
7
+ s.date = %q{2009-01-29}
8
+ s.authors = ["David Nyffenegger"]
9
+ s.email = %q{vidl@sunrise.ch}
10
+ s.summary = %q{Davids toolbox to speedup development with rails (mainly view).}
11
+ s.homepage = %q{http://github.com/vidl/toolbox}
12
+ s.description = %q{This toolbox goes in the direction of the django admin interface.}
13
+ s.files = FileList["[A-Z]*", "{lib,locale,view}/**/*"]
14
+
15
+ # rdoc
16
+ s.has_rdoc = true
17
+ #s.extra_rdoc_files = %w(README.rdoc MIT-LICENSE.txt)
18
+
19
+ s.add_dependency "activesupport", ">= 2.1.1"
20
+ s.add_dependency "actionpack", ">= 2.1.1"
21
+ s.add_dependency "actionpack", ">= 2.1.1"
22
+ s.add_dependency "gettext", ">= 1.92.0"
23
+ s.add_dependency "mislav-will_paginate", ">= 2.3.3"
24
+
25
+ end
26
+
27
+ desc "Generate a gemspec file for GitHup"
28
+ task :gemspec do
29
+ File.open("#{spec.name}.gemspec", 'w') do |f|
30
+ f.write spec.to_ruby
31
+ end
32
+ end
33
+
34
+ desc "Create the gem"
35
+ task :gem => [:makemo, :gemspec] do
36
+ `gem build #{spec.name}.gemspec`
37
+ end
38
+
39
+ desc "Generate RDoc"
40
+ task :doc do
41
+ system "hanna -x Rakefile -x spec --title 'Toolbox #{Toolbox::VERSION} API Documentation'"
42
+ end
43
+
44
+ desc "Update pot/po files."
45
+ task :updatepo do
46
+ require 'gettext/utils' #HERE!
47
+ GetText.update_pofiles("#{spec.name}", Dir.glob("{lib}/**/*.{rb,erb,rjs}"), "#{spec.name} #{Toolbox::VERSION}")
48
+ end
49
+
50
+ desc "Create mo-files"
51
+ task :makemo do
52
+ require 'gettext/utils' #HERE!
53
+ GetText.create_mofiles(true, "po", "locale")
54
+ end
data/lib/dirs.rb ADDED
@@ -0,0 +1,9 @@
1
+
2
+ module Toolbox
3
+ module Dirs
4
+ GEM_ROOT = File.expand_path("../", File.dirname(__FILE__)) unless defined? GEM_ROOT
5
+ LOCALE = File.join(GEM_ROOT, '/locale') unless defined? LOCALE
6
+ VIEW = File.join(GEM_ROOT, '/view') unless defined? VIEW
7
+
8
+ end
9
+ end
@@ -0,0 +1,185 @@
1
+
2
+ module Toolbox
3
+
4
+ class WidgetList
5
+ attr_reader :widgets
6
+
7
+ def initialize model_name, widgets, type
8
+ @widgets = []
9
+ last = nil
10
+ h = nil
11
+ widgets.each do |widget|
12
+ h = { :name => last, :model_name => model_name}
13
+ if widget.is_a? Hash
14
+ raise 'Before an option hash there must be a widget name (string or symbol)' unless last
15
+ raise 'The item before a option hash must be a string or a symbol' unless last.is_a?(String) || last.is_a?(Symbol)
16
+ h.merge! widget
17
+ end
18
+ @widgets << type.new(h) if last && !last.is_a?(Hash)
19
+ last = widget
20
+ end
21
+ # add the last field, if it's a symbol
22
+ if last && !last.is_a?(Hash)
23
+ h = { :name => last, :model_name => model_name}
24
+ @widgets << type.new(h)
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ class EditConfig
31
+ attr_reader :controlsets
32
+
33
+ def initialize model_name, config
34
+ @controlsets = []
35
+ config.each do |controlset|
36
+ h = {}
37
+ h.merge! controlset
38
+ h[:widget_list] = WidgetList.new(model_name, controlset[:widget_list], ControlConfig)
39
+ h[:collection_config] = CollectionConfig.new controlset[:collection_config] if controlset[:collection_config]
40
+ @controlsets << WidgetSetConfig.new(h)
41
+ end
42
+ end
43
+ end
44
+
45
+ class ShowConfig
46
+ attr_reader :fieldsets
47
+ attr_reader :embedded_lists
48
+
49
+ def initialize model_name, config
50
+ @fieldsets = []
51
+ config[:fieldsets].each do |fieldset|
52
+ h = {}
53
+ h.merge! fieldset
54
+ h[:widget_list] = WidgetList.new(model_name, fieldset[:widget_list], FieldConfig)
55
+ h[:collection_config] = CollectionConfig.new fieldset[:collection_config] if fieldset[:collection_config]
56
+ @fieldsets << WidgetSetConfig.new(h)
57
+ end
58
+ @embedded_lists = []
59
+ prefixes = {}
60
+ config[:embedded_lists] = config[:embedded_lists] || []
61
+ config[:embedded_lists].each do |list|
62
+ h = {}
63
+ h.merge! list
64
+ h[:collection_config] = CollectionConfig.new list[:collection_config]
65
+ h[:widget_list] = WidgetList.new(h[:collection_config].type.name, list[:widget_list], FieldConfig)
66
+ c = EmbeddedListConfig.new(h)
67
+ raise "Prefix #{c.prefix} is already used by #{prefixes[c.prefix].label}" if prefixes[c.prefix]
68
+ prefixes[c.prefix] = c
69
+ @embedded_lists << c
70
+ end
71
+ end
72
+ end
73
+
74
+ # The base class of the configuration classes.
75
+
76
+ class Config
77
+ protected
78
+ # Use this methods to easily define which options the configuration class supports.
79
+ # This is done by hash, where each key defines a single option by its name.
80
+ # The value is a hash that may contain the following keys:
81
+ # - :required boolean, if true, this option is mandatory, default is false
82
+ # - :type array of class-names. If set, an exception is raised if the given value is not of the specified type
83
+ # - :values array of allowed values. If set, an exception is raised if the given value is not withing the specified values.
84
+ # - :default a default value, that is set, if the option is not given. Otherwise, nil is assgined
85
+ def self.define_options options
86
+ # attr_reader for each option
87
+ options.each_key { |k| attr_reader k }
88
+ write_inheritable_attribute 'options', options || {}
89
+ end
90
+
91
+ # create the constructor out of the options definition
92
+ def initialize vars
93
+ vars.each_pair do |k,v|
94
+ if respond_to? k.to_sym # set only, if there is an accessor
95
+ k = '@' + k.to_s
96
+ instance_variable_set(k.to_sym,v)
97
+ else
98
+ raise "Unkown option #{k.to_s} (#{self.class.name.to_s})"
99
+ end
100
+ end
101
+ # check against the definition
102
+ self.class.read_inheritable_attribute('options').each_pair do |name, opt|
103
+ val = vars[name]
104
+ class_name = "(#{self.class.name.to_s}: #{val.inspect})"
105
+ raise "#{name} is required #{class_name}" if opt[:required] && !val
106
+ raise "#{name} is #{val.class.to_s} and not of #{opt[:type].join(' or ')} #{class_name}" if opt[:type] && val && !opt[:type].include?(val.class)
107
+ raise "#{name} is not in #{opt[:values].join(', ')} #{class_name}" if opt[:values] && val && !opt[:values].include?(val)
108
+ instance_variable_set("@#{name.to_s}".to_sym, opt[:default] || nil) unless val
109
+ end
110
+ end
111
+ end
112
+
113
+ # Holds the configuration for a collection. This defines how to render a widget if it's not a simple
114
+ # type but a collection.
115
+ class CollectionConfig < Config
116
+ define_options :type => { :type => [Class], :required => true},
117
+ :model_method => { :type => [Symbol], :required => true},
118
+ :suppress_table_header => { :default => false},
119
+ :hide_empty => { :default => false}
120
+ end
121
+
122
+ # Holds the configuration for a set of widgets. Widgets are either controls of a form
123
+ # or un-editable fields.
124
+ class WidgetSetConfig < Config
125
+ define_options :widget_list => { :type => [WidgetList], :required => true},
126
+ :label => { :type => [String], :required => true},
127
+ :collection_config => { :type => [CollectionConfig] },
128
+ :condition => { :type => [Proc] } # if set, the fieldset is only rendered, if the proc returns true
129
+ end
130
+
131
+ # Holds the configuration for an embedded list
132
+ class EmbeddedListConfig < Config
133
+ define_options :widget_list => { :type => [WidgetList], :required => true},
134
+ :label => { :type => [String], :required => true},
135
+ :collection_config => { :type => [CollectionConfig], :required => true },
136
+ :prefix => { :type => [String], :required => true},
137
+ :include_assoc => { :type => [Array]}
138
+ end
139
+
140
+ class ActionConfig < Config
141
+ define_options :label => { :type => [String] },
142
+ :name => { :required => true, :type => [Symbol] },
143
+ :model_name => { :required => true, :type => [String] }
144
+ end
145
+
146
+ # Holds the configuration for a from control
147
+ class ControlConfig < Config
148
+ define_options :model_method => { :type => [Symbol, Proc]},
149
+ :text_method => { :type => [Symbol]},
150
+ :suppress_label => { :default => false },
151
+ :label => { :type => [String] },
152
+ :name => { :required => true, :type => [Symbol] },
153
+ :model_name => { :required => true, :type => [String] },
154
+ :type => { :type => [Symbol], :default => :textfield, :values => [:select, :collection_select, :auto_complete, :textfield, :radio, :date, :check_box]},
155
+ :url => { :type => [Symbol]},
156
+ :size => { :type => [Fixnum]},
157
+ :values => { :type => [Array]}
158
+ end
159
+
160
+ # Holds the configuration for a non-editable field
161
+ class FieldConfig < Config
162
+ define_options :model_method => { :type => [Symbol, Proc]},
163
+ :hide_empty => { :default => false },
164
+ :suppress_label => { :default => false },
165
+ :suppress_link => { :default => false },
166
+ :suppress_sorting => { :default => false },
167
+ :suppress_context_menu => { :default => false},
168
+ :order_by => { :type => [String] },
169
+ :label => { :type => [String] },
170
+ :join => {},
171
+ :suffix => {},
172
+ :name => { :required => true, :type => [String, Symbol] },
173
+ :model_name => { :required => true, :type => [String] },
174
+ :trunc => { :type => [Fixnum]}
175
+
176
+
177
+ def ==(s)
178
+ (s.is_a?(Hash) && s[:name] && @name.to_s == s[:name].to_s) ||
179
+ (s.is_a?(Symbol) && @name.to_s == s.to_s) ||
180
+ (s.is_a?(String) && @name.to_s == s) ||
181
+ (s.is_a?(FieldConfig) && s.name.to_s == @name.to_s)
182
+ end
183
+ end
184
+
185
+ end
@@ -0,0 +1,315 @@
1
+ require File.expand_path("../dirs", File.dirname(__FILE__))
2
+
3
+ module Toolbox
4
+
5
+ #
6
+ # Defines the default action.
7
+ #
8
+ module DefaultController
9
+
10
+ def self.setup #:nodoc:
11
+ ActionController::Base.send(:extend, ClassMethods)
12
+ #ActionController::Base.send(:include, InstanceMethods)
13
+ ActionView::Base.send(:include, HelperMethods)
14
+ end
15
+
16
+ module HelperMethods
17
+ def get_record
18
+ instance_variable_get ('@' + controller.model_name).to_sym
19
+ end
20
+
21
+ def context_menu_link(rec, context = nil)
22
+ context_records = []
23
+ # get the record in show-mode
24
+ context_records << get_record if get_record.is_a? ActiveRecord::Base
25
+ context_records << context if context
26
+ p = {}
27
+ context_records.each { |r| p["#{r.class.name.to_s}_id".to_sym] = r }
28
+ ctx_url =send("context_menu_#{rec.class.to_s.underscore}_path".to_sym, rec, p)
29
+ link_to_function('&#9660;', "show_context_menu(event, '#{ctx_url}')")
30
+ end
31
+
32
+ # returns html necessary to load javascript and css to make the toolbox work
33
+ def toolbox_includes()
34
+ return "" if @toolbox_already_included
35
+ @toolbox_already_included = true
36
+
37
+
38
+ js = javascript_include_tag "toolbox-#{Toolbox::VERSION}/toolbox", "toolbox-#{Toolbox::VERSION}/popup"
39
+ cs = stylesheet_link_tag 'toolbox/context_menu', 'toolbox/popup', 'toolbox/toolbox'
40
+ js + "\n" + cs + "\n"
41
+ end
42
+
43
+
44
+ end
45
+
46
+ module ClassMethods
47
+
48
+ # The listed associations are included in when using find together with
49
+ # search fields. Thus if a search field defintion contains "foreign" fields
50
+ # one may include the corresponding association.
51
+ def include_assoc *assoc
52
+ write_inheritable_attribute('include_assoc', assoc)
53
+ end
54
+
55
+ end
56
+
57
+ module InstanceMethods #:nodoc:all
58
+
59
+ def index
60
+ obj = paging
61
+ instance_variable_set :@recs, obj
62
+
63
+ respond_to do |format|
64
+ format.html { render :template => 'toolbox/index' }
65
+ format.xml { render :xml => obj }
66
+ end
67
+ end
68
+
69
+ def show
70
+ obj = model_class.find(params[:id])
71
+ set_record obj
72
+ respond_to do |format|
73
+ format.html { render :template => 'toolbox/show' }
74
+ format.xml { render :xml => obj }
75
+ end
76
+ end
77
+
78
+ def new
79
+ # Handle dublicate command
80
+ param_name = "#{model_name}_id".to_sym
81
+ attr = {}
82
+ attr = model_class.find(params[param_name]).attributes if params[param_name]
83
+ obj = model_class.new attr
84
+
85
+ set_record obj
86
+
87
+ # context support
88
+ # set all _id fields, if available from params
89
+ params.each_pair do | key,value |
90
+ if key.to_s.ends_with? '_id' and obj.respond_to? key + '='
91
+ obj.send(key + '=', value)
92
+ end
93
+ end
94
+
95
+ respond_to do |format|
96
+ format.html { render :template => 'toolbox/new' }
97
+ format.xml { render :xml => obj }
98
+ format.js do
99
+ render :update do |page|
100
+ locals = {:widgetsets => controller.edit_config(obj).controlsets, :dialog => true }
101
+ page.replace_html 'dialog_id', :partial => "toolbox/form", :object => obj, :locals => locals
102
+ page << "$('dialog_id').popup.show();"
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def edit
109
+ obj = model_class.find(params[:id])
110
+ set_record obj
111
+
112
+ respond_to do |format|
113
+ format.html { render :template => 'toolbox/edit' }
114
+ format.js do
115
+ render :update do |page|
116
+ locals = {:widgetsets => controller.edit_config(obj).controlsets, :dialog => true }
117
+ page.replace_html 'dialog_id', :partial => "toolbox/form", :object => obj, :locals => locals
118
+ page << "$('dialog_id').popup.show();"
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ def create
125
+ if params[:save] # save or ok clicked
126
+ p = params[model_name.to_sym]
127
+ preprocess_autocomplete nil, p
128
+ obj = model_class.new p
129
+ set_record obj
130
+
131
+ respond_to do |format|
132
+ if obj.save
133
+ flash[:notice] = Messages.created % { :object => s_(controller_name.singularize.humanize.downcase)}
134
+ format.html { redirect_to(obj) }
135
+ format.js do
136
+ render :update do
137
+ |page| page << 'window.location.reload();' # TODO improvment?
138
+ end
139
+ end
140
+ format.xml { render :xml => obj, :status => :created, :location => obj }
141
+ else
142
+ format.html { render :template => "toolbox/new" }
143
+ format.xml { render :xml => obj.errors, :status => :unprocessable_entity }
144
+ format.js do
145
+ render :update do |page|
146
+ locals = {:widgetsets => controller.edit_config(obj).controlsets, :dialog => true }
147
+ page.replace_html 'dialog_id', :partial => "toolbox/form", :object => obj, :locals => locals
148
+ end
149
+ end
150
+ end
151
+ end
152
+ else # cancel clicked
153
+ redirect_to eval("#{controller_name}_path")
154
+ end
155
+ end
156
+
157
+
158
+ def update
159
+ obj = model_class.find(params[:id])
160
+ set_record obj
161
+
162
+ if params[:save]
163
+ par = params[model_name.to_sym]
164
+ preprocess_autocomplete obj, par
165
+ # Ensure that the the collection attributes exist - create them if not
166
+ # This is needed, if all collection items have been deleted.
167
+ edit_config(obj).controlsets.each do |controlset|
168
+ if controlset.collection_config
169
+ attr = Toolbox::Helpers.collection_attribute_name(controlset.collection_config, obj)
170
+ par[attr] ||= {}
171
+ end
172
+ end
173
+
174
+ respond_to do |format|
175
+ if obj.update_attributes(par)
176
+ flash[:notice] = Messages.updated % { :object => s_(controller_name.singularize.humanize.downcase)}
177
+ format.html { redirect_to(obj) }
178
+ format.xml { head :ok }
179
+ format.js do
180
+ render :update do |page|
181
+ page << 'window.location.reload();' # TODO improve ?
182
+ end
183
+ end
184
+ else
185
+ format.html { render :template => "toolbox/edit" }
186
+ format.xml { render :xml => obj.errors, :status => :unprocessable_entity }
187
+ format.js do
188
+ render :update do |page|
189
+ locals = {:widgetsets => controller.edit_config(obj).controlsets, :dialog => true }
190
+ page.replace_html 'dialog_id', :partial => "toolbox/form", :object => obj, :locals => locals
191
+ end
192
+ end
193
+ end
194
+ end
195
+ else # cancel clicked
196
+ respond_to do |format|
197
+ format.html { redirect_to(obj) }
198
+ format.xml { head :ok }
199
+ end
200
+ end
201
+ end
202
+
203
+ def destroy
204
+ obj = model_class.find(params[:id])
205
+ set_record obj
206
+ obj.destroy
207
+
208
+ respond_to do |format|
209
+ format.html { redirect_to(send(controller_name + '_url')) }
210
+ format.xml { head :ok }
211
+ format.js do
212
+ render :update do |page|
213
+ page << 'window.location.reload();' # TODO: improve?
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def context_menu
220
+ rec = model_class.find(params[:id])
221
+ set_record rec
222
+ context = {}
223
+ params.select { |k,v| k.ends_with? '_id' }.each {|a| context[a[0]] = a[1] }
224
+ locals = { :context => context, :widget_list => menu_config(rec, context)}
225
+ respond_to do |format|
226
+ format.html { render :partial => "toolbox/context_menu", :object => rec, :locals => locals }
227
+ format.js do
228
+ render :update do |page|
229
+ page.replace_html 'context_menu_id', :partial => "toolbox/context_menu", :object => rec, :locals => locals
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ def model_name
236
+ controller_name.singularize
237
+ end
238
+
239
+ def model_class
240
+ controller_name.classify.constantize
241
+ end
242
+
243
+ protected
244
+
245
+ #def load_config
246
+ # @config = "#{controller_name.camelcase}ViewConfig".constantize.get_instance unless defined? @config
247
+ #end
248
+
249
+ # check the consitency between the visible input-field
250
+ # and the invisible id field
251
+ def preprocess_autocomplete rec, params
252
+ edit_config(rec).controlsets.each do |controlset|
253
+ controlset.widget_list.widgets.each do |widget|
254
+ if widget.type == :auto_complete
255
+ value = params.delete widget.name
256
+ params[widget.name.to_s.foreign_key] = nil if value == '' || value == nil
257
+ end
258
+ end
259
+ end
260
+ params
261
+ end
262
+
263
+ # do the paging for the index
264
+ def paging
265
+ filter = (params[:query] || params[:filter] || '')
266
+ cond = Toolbox::Searching.create_search_condition filter, search_fields
267
+ order = Toolbox::Sorting.order_by params, index_config.widgets
268
+ options = { :page => params[:page], :conditions => cond }
269
+ options[:order] = order if order
270
+ options[:include] = include_assoc if include_assoc
271
+ model_class.paginate options
272
+ end
273
+
274
+ private
275
+
276
+ def set_record obj
277
+ instance_variable_set "@#{model_name}".to_sym, obj
278
+ end
279
+
280
+ def include_assoc
281
+ self.class.read_inheritable_attribute('include_assoc')
282
+ end
283
+ end
284
+
285
+ end
286
+
287
+ # needed for not to conflict with the textdomain from the application controller
288
+ module Messages #:nodoc:all
289
+ include GetText::Rails
290
+ bindtextdomain('toolbox', :path => Toolbox::Dirs::LOCALE)
291
+ def self.created
292
+ _('%{object} was successfully created.')
293
+ end
294
+ def self.updated
295
+ _('%{object} was successfully updated.')
296
+ end
297
+
298
+ def self.create
299
+ _('Create')
300
+ end
301
+
302
+ def self.save
303
+ _('Save')
304
+ end
305
+
306
+ def self.cancel
307
+ _('Cancel')
308
+ end
309
+
310
+ def self.nothing_found
311
+ _('Nothing found')
312
+ end
313
+ end
314
+
315
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module Toolbox
3
+ module Helpers
4
+
5
+ def self.collection_attribute_name collection_config, rec
6
+ new_or_existing = rec.new_record? ? 'new' : 'existing'
7
+ "#{new_or_existing}_#{collection_config.type.name.to_s.underscore}_attributes"
8
+ end
9
+
10
+ end
11
+ end