vidl-toolbox 0.0.1

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