schofield 0.1.5 → 0.2.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 (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