schofield 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.bundle/config +2 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +28 -0
  4. data/{LICENSE → LICENSE.txt} +1 -1
  5. data/README.rdoc +11 -9
  6. data/Rakefile +28 -24
  7. data/VERSION +1 -1
  8. data/lib/generators/schofield/association.rb +94 -0
  9. data/lib/generators/schofield/attribute.rb +76 -0
  10. data/lib/generators/schofield/attributes.rb +204 -0
  11. data/lib/generators/schofield/level.rb +287 -0
  12. data/lib/generators/schofield/levels.rb +74 -0
  13. data/lib/generators/schofield/responses.rb +46 -0
  14. data/lib/generators/schofield/routes.rb +46 -0
  15. data/lib/generators/schofield/schofield_generator.rb +124 -0
  16. data/lib/generators/templates/controller.erb +25 -0
  17. data/lib/generators/templates/form.erb +31 -0
  18. data/lib/generators/templates/index.erb +5 -0
  19. data/lib/generators/templates/join_controller.erb +15 -0
  20. data/lib/generators/templates/model.erb +109 -0
  21. data/lib/generators/templates/show.erb +21 -0
  22. data/schofield.gemspec +48 -46
  23. data/spec/spec_helper.rb +7 -4
  24. metadata +82 -47
  25. data/.gitignore +0 -21
  26. data/generators/schofield_controller/schofield_controller_generator.rb +0 -157
  27. data/generators/schofield_controller/templates/controller_spec.rb +0 -25
  28. data/generators/schofield_controller/templates/helper_spec.rb +0 -11
  29. data/generators/schofield_controller/templates/index_partial.rb +0 -26
  30. data/generators/schofield_controller/templates/nested/controller.rb +0 -106
  31. data/generators/schofield_controller/templates/nested/edit.rb +0 -5
  32. data/generators/schofield_controller/templates/nested/index.rb +0 -3
  33. data/generators/schofield_controller/templates/nested/new.rb +0 -1
  34. data/generators/schofield_controller/templates/nested/show.rb +0 -12
  35. data/generators/schofield_controller/templates/unnested/controller.rb +0 -94
  36. data/generators/schofield_controller/templates/unnested/edit.rb +0 -5
  37. data/generators/schofield_controller/templates/unnested/index.rb +0 -3
  38. data/generators/schofield_controller/templates/unnested/new.rb +0 -1
  39. data/generators/schofield_controller/templates/unnested/show.rb +0 -12
  40. data/generators/schofield_controller/templates/view_spec.rb +0 -12
  41. data/generators/schofield_form/schofield_form_generator.rb +0 -22
  42. data/generators/schofield_form/templates/view__form.html.haml +0 -11
  43. data/lib/schofield.rb +0 -111
  44. data/lib/schofield/tasks.rb +0 -3
  45. data/lib/schofield/tasks/schofield.rake +0 -7
@@ -0,0 +1,287 @@
1
+ module Schofield
2
+
3
+ module Generators
4
+
5
+ class Level
6
+
7
+ COLUMNS_TO_IGNORE = %w( ^created_at$ ^updated_at$ _content_type$ _file_size$ _updated_at$ _type$ ^type$ ^crypted_password$ ^password_salt$ ^persistence_token$ ^perishable_token$ ^login_count$ ^failed_login_count$ ^last_request_at$ ^current_login_at$ ^last_login_at$ ^current_login_ip$ ^last_login_ip$ )
8
+
9
+ class << self; attr_reader :columns_to_ignore end
10
+
11
+ def self.tables_to_ignore= tables
12
+ @columns_to_ignore = ( COLUMNS_TO_IGNORE + tables.map{ |m| "^#{m.singularize}_id$" } )
13
+ end
14
+
15
+
16
+ attr_reader :name, :parent_associations, :attributes, :superclass
17
+ attr_accessor :child_associations, :subclasses
18
+
19
+ delegate :validations,
20
+ :validations?,
21
+ :attr_protecteds,
22
+ :attr_protecteds?,
23
+ :acts_as_markdowns,
24
+ :acts_as_markdowns?,
25
+ :attachments?,
26
+ :to_s_string,
27
+ :attached_files,
28
+ :to => :attributes
29
+
30
+
31
+
32
+
33
+ def initialize model, superclass=nil
34
+ @model = model
35
+ @name = model.name.underscore
36
+ @human_name = @name.gsub('_', ' ')
37
+ @superclass = superclass
38
+ @subclasses = []
39
+ @parent_associations = []
40
+ @child_associations = []
41
+ add_parent_associations
42
+ add_attributes
43
+ end
44
+
45
+
46
+ # Inheritence
47
+
48
+ def subclass?
49
+ @superclass.present?
50
+ end
51
+
52
+ def superclass?
53
+ @subclasses.any?
54
+ end
55
+
56
+
57
+ # Join table
58
+
59
+ def join?
60
+ @join ||= @model.columns.select { |c| c.name.match(/_id$/) }.length == 2 && @model.columns.select { |c| !%w( id created_at updated_at position ).include?(c.name) }.length == 2
61
+ end
62
+
63
+ def other_parent_name parent_name
64
+ @parent_associations.find{ |a| a.parent_name != parent_name }.parent_name
65
+ end
66
+
67
+
68
+ # Polymorphism
69
+
70
+ def polymorphic?
71
+ @polymorphic ||= polymorphic_name.present?
72
+ end
73
+
74
+ def polymorphic_name
75
+ match_data = nil
76
+ @polymorphic_name ||= @model.columns.find { |c| match_data = c.name.match(/^(.+)_type$/) } ? match_data[1] : nil
77
+ end
78
+
79
+
80
+ # Nesting
81
+
82
+ def nested?
83
+ @parent_associations.find(&:nest?).present?
84
+ end
85
+
86
+ def nests?
87
+ @child_associations.find(&:nest?).present?
88
+ end
89
+
90
+ def nested_associations
91
+ if superclass? then []
92
+ else
93
+ associations = @child_associations + ( subclass? ? @superclass.child_associations : [] )
94
+ end
95
+ end
96
+
97
+ def nested_levels
98
+ nested_associations.select(&:nest?).map(&:child)
99
+ end
100
+
101
+
102
+ # Associations and cardinality
103
+
104
+ def belongs_to?
105
+ @parent_associations.any?
106
+ end
107
+
108
+ def belongs_to_one_names
109
+ @parent_associations.select(&:one_to_one?).map(&:parent_name)
110
+ end
111
+
112
+ def belongs_to_one?
113
+ @parent_associations.find(&:one_to_one?).present?
114
+ end
115
+
116
+ def has_ones?
117
+ @child_associations.find(&:one_to_one?).present?
118
+ end
119
+
120
+ def has_manies?
121
+ @child_associations.find(&:one_to_many?).present?
122
+ end
123
+
124
+ def has_ones
125
+ @child_associations.select(&:one_to_one?).map(&:child)
126
+ end
127
+
128
+ def has_manies
129
+ @child_associations.select(&:one_to_many?).map(&:child)
130
+ end
131
+
132
+
133
+ # Combos
134
+
135
+ def routes?
136
+ !nested? && !superclass? && !belongs_to_one? && !join?
137
+ end
138
+
139
+ def controllers?
140
+ !superclass? && name != 'user' && !belongs_to_one?
141
+ end
142
+
143
+ def views?
144
+ controllers? && !join?
145
+ end
146
+
147
+ def models?
148
+ name != 'user'
149
+ end
150
+
151
+ def tables?
152
+ !belongs_to_one? && !superclass?
153
+ end
154
+
155
+
156
+
157
+ # Sortable
158
+
159
+ def sortable?
160
+ @sortable
161
+ end
162
+
163
+
164
+ # Form
165
+
166
+ def multipart?
167
+ attachments? || has_ones.find(&:attachments?).present?
168
+ end
169
+
170
+
171
+ # Association ancestry
172
+
173
+ def ancestry
174
+ level = self
175
+ nested_ins = []
176
+ next_name = nil
177
+ while level
178
+ if next_name
179
+ this_name = next_name
180
+ next_name = nil
181
+ else
182
+ this_name = level.name
183
+ end
184
+ nested_ins << this_name
185
+ next_name = level.polymorphic_name if level.polymorphic?
186
+ level = level.parent_associations.select(&:nest?).map(&:parent).first
187
+ # polymorphic model could have more than one parent!!!
188
+ end
189
+ nested_ins.reverse
190
+ end
191
+
192
+
193
+ # Form fields
194
+
195
+ def attribute_of_nesting_parent? attribute
196
+ attribute.model_name && parent_associations.select(&:nest?).map(&:child_name).include?(attribute.model_name)
197
+ end
198
+
199
+ def polymorphic_attribute? attribute
200
+ polymorphic_name && attribute.model_name == polymorphic_name
201
+ end
202
+
203
+ def form_field? attribute
204
+ attribute.name != 'position' && !polymorphic_attribute?(attribute) && !attribute_of_nesting_parent?(attribute)
205
+ end
206
+
207
+
208
+
209
+ private
210
+
211
+ def add_attributes
212
+ @attributes = Attributes.new
213
+ @model.columns.each do |column|
214
+ add_attribute(column) unless ignore?(column) || (polymorphic? && column.name =~ /^#{polymorphic_name}_id$/)
215
+ end
216
+ end
217
+
218
+ def add_attribute column
219
+ attribute = @attributes.new_attribute(column, belongs_to_one_names.include?(column.name.gsub(/_id$/, '')))
220
+ set_sortable if attribute.name == 'position'
221
+ end
222
+
223
+ def set_sortable
224
+ unless superclass?
225
+ @sortable = true
226
+ Levels.sortable = @name
227
+ end
228
+ end
229
+
230
+ # Nesting refers to nested routes, embedding refers to nested_attributes_for
231
+ # Assuming that a polymorphic model has no parents other than it's polymorphically associated parents
232
+ # If model has a one-to-one relationship with a parent, no nesting will occur as child will be embedded in parent
233
+ # Only allowing child to be nested under one parent or if polymorphic, the polymorphically associated parents
234
+ def add_parent_associations
235
+
236
+ if polymorphic?
237
+
238
+ answer = Responses.get("Which models are #{polymorphic_name}?")
239
+ answer.split(/[^\w]+/).map(&:underscore).each do |parent_name|
240
+ @parent_associations << Association.new(self, parent_name, one_to_one?(polymorphic_name), polymorphic_name)
241
+ end
242
+
243
+ else
244
+
245
+ any_one_to_ones = false
246
+ @model.columns.each do |column|
247
+ if (match_data = column.name.match(/^(.+)_id$/)).present?
248
+ parent_name = match_data[1]
249
+ is_one_to_one = one_to_one?(parent_name)
250
+ any_one_to_ones = true if is_one_to_one
251
+ @parent_associations << Association.new(self, parent_name, is_one_to_one)
252
+ end
253
+ end
254
+
255
+ if @parent_associations.any? && !any_one_to_ones
256
+ @parent_associations.length == 1 ? ask_if_nested : ask_where_to_nest
257
+ end
258
+ end
259
+ end
260
+
261
+ def one_to_one? parent_name
262
+ @one_to_one ||= %w( y yes ).include?(Responses.get("#{parent_name.gsub('_', ' ')} HAS ONE #{@human_name}? [N]").downcase)
263
+ end
264
+
265
+ def ask_if_nested
266
+ question = "Do you wish to nest #{@human_name} in #{@parent_associations.first.parent_name}? [yes]"
267
+ @parent_associations.first.nest = true unless %w( n no ).include?(Responses.get(question).downcase.strip)
268
+ end
269
+
270
+ def ask_where_to_nest
271
+ question = "Where do you wish to nest #{@human_name}? nowhere(n), "
272
+ question += @parent_associations.enum_with_index.map{ |n,i| "#{n.parent_name.gsub('_', ' ')}(#{i})" }.join(', ') + ' [n]'
273
+ answer = Responses.get(question)
274
+ @parent_associations[answer.to_i].nest = true unless answer == 'n' || answer.blank?
275
+ end
276
+
277
+ def ignore? column
278
+ self.class.columns_to_ignore.each do |string|
279
+ return true if column.name.match(/#{string}/)
280
+ end
281
+ column.primary
282
+ end
283
+
284
+ end
285
+
286
+ end
287
+ end
@@ -0,0 +1,74 @@
1
+ module Schofield
2
+
3
+ module Generators
4
+
5
+ class Levels
6
+
7
+ class << self; attr_reader :all, :hierarchy, :sortables end
8
+
9
+ @all = []
10
+ @names = {}
11
+ @sortables = {}
12
+
13
+
14
+
15
+ def self.models= models
16
+ models.each do |model|
17
+ if polymorphic_model?(model)
18
+ superclass = add_level(model)
19
+ subclasses(model).each do |subclass|
20
+ add_level(subclass, superclass)
21
+ superclass.subclasses << subclass
22
+ end
23
+ else
24
+ add_level(model)
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.sortables
30
+ @sortables.keys
31
+ end
32
+
33
+ def self.sortable=sortable
34
+ @sortables[sortable] = true
35
+ end
36
+
37
+ def self.find name
38
+ @all[@names[name]]
39
+ end
40
+
41
+
42
+
43
+ private
44
+
45
+
46
+ def self.polymorphic_model? model
47
+ model.columns.find { |c| c.name == 'type' }.present?
48
+ end
49
+
50
+ def self.subclasses model
51
+ begin
52
+ answer = Responses.get "Which models extend #{model.name}?"
53
+ subclasses = answer.split(/[^\w]+/).map{ |a| a.camelize.constantize }
54
+ raise StandardError if subclasses.blank?
55
+ rescue NameError => error
56
+ Responses.say "Could not find models: #{error}"
57
+ retry
58
+ rescue StandardError
59
+ Responses.say "You must answer!"
60
+ retry
61
+ end
62
+ subclasses
63
+ end
64
+
65
+ def self.add_level model, superclass=nil
66
+ level = Level.new(model, superclass)
67
+ @all << level
68
+ @names[model.name.underscore] = @all.length - 1
69
+ level
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,46 @@
1
+ module Schofield
2
+
3
+ module Generators
4
+
5
+ class Responses
6
+
7
+ class << self; attr_accessor :generator, :re_ask end
8
+
9
+ @file = File.join(Rails.root, 'tmp', 'answers.txt')
10
+
11
+
12
+ def self.get question
13
+ @question = question
14
+ if re_ask || (answer = past_answer).nil?
15
+ answer = ask
16
+ end
17
+ answer || ''
18
+ end
19
+
20
+ def self.ask
21
+ re_ask = true
22
+ answer = generator.ask(@question)
23
+ @answers ||= {}
24
+ @answers[@question] = answer || ''
25
+ end
26
+
27
+ def self.past_answer
28
+ self.past_answers[@question]
29
+ end
30
+
31
+ def self.past_answers
32
+ @answers ||= File.exists?(@file) ? File.open(@file, 'rb') { |f| Marshal.load(f) } : {}
33
+ end
34
+
35
+ def self.save
36
+ File.open(@file, 'wb') { |io| Marshal.dump(@answers, io) }
37
+ end
38
+
39
+ def self.say string
40
+ generator.say(string)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ module Schofield
2
+
3
+ module Generators
4
+
5
+ class Routes
6
+
7
+ def self.add_routes levels, depth=2
8
+ childless = []
9
+ levels.each do |level|
10
+ if level.join?
11
+ @joins << level
12
+ elsif level.nests?
13
+ @routes << route_string([level], depth)
14
+ add_routes(level.nested_levels, depth+1)
15
+ @routes << "#{' ' * depth}end" + (depth == 2 ? "\n" : '')
16
+ else
17
+ childless << level
18
+ end
19
+ end
20
+ @routes << route_string(childless, depth) if childless.any?
21
+ end
22
+
23
+ def self.route_string levels, depth
24
+ string = ' ' * depth
25
+ string += "resources "
26
+ string += ':' + levels.map{ |level| level.name.tableize }.join(', :')
27
+ string += ", "
28
+ string += ":except => #{depth == 2 ? ':edit' : '[:index, :edit]'}"
29
+ string += levels.length == 1 && levels.first.nests? ? ', :shallow => true do' : ''
30
+ end
31
+
32
+ def self.generate
33
+ @routes = []
34
+ @joins = []
35
+ add_routes(Levels.all.select(&:routes?))
36
+ @routes.push "\n resources :#{@joins.map{ |s| s.name.tableize }.join(', :')}, :only => [:create, :delete]" if @joins.any?
37
+ @routes.push "\n resources :#{Levels.sortables.map{ |s| s.tableize }.join(', :')}, :only => [] do\n post :sort, :on => :collection\n end" if Levels.sortables.any?
38
+ @routes.unshift ' namespace :admin do'
39
+ @routes.push ' end'
40
+ @routes.join("\n") + "\n\n"
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end