schofield 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +2 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +28 -0
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.rdoc +11 -9
- data/Rakefile +28 -24
- data/VERSION +1 -1
- data/lib/generators/schofield/association.rb +94 -0
- data/lib/generators/schofield/attribute.rb +76 -0
- data/lib/generators/schofield/attributes.rb +204 -0
- data/lib/generators/schofield/level.rb +287 -0
- data/lib/generators/schofield/levels.rb +74 -0
- data/lib/generators/schofield/responses.rb +46 -0
- data/lib/generators/schofield/routes.rb +46 -0
- data/lib/generators/schofield/schofield_generator.rb +124 -0
- data/lib/generators/templates/controller.erb +25 -0
- data/lib/generators/templates/form.erb +31 -0
- data/lib/generators/templates/index.erb +5 -0
- data/lib/generators/templates/join_controller.erb +15 -0
- data/lib/generators/templates/model.erb +109 -0
- data/lib/generators/templates/show.erb +21 -0
- data/schofield.gemspec +48 -46
- data/spec/spec_helper.rb +7 -4
- metadata +82 -47
- data/.gitignore +0 -21
- data/generators/schofield_controller/schofield_controller_generator.rb +0 -157
- data/generators/schofield_controller/templates/controller_spec.rb +0 -25
- data/generators/schofield_controller/templates/helper_spec.rb +0 -11
- data/generators/schofield_controller/templates/index_partial.rb +0 -26
- data/generators/schofield_controller/templates/nested/controller.rb +0 -106
- data/generators/schofield_controller/templates/nested/edit.rb +0 -5
- data/generators/schofield_controller/templates/nested/index.rb +0 -3
- data/generators/schofield_controller/templates/nested/new.rb +0 -1
- data/generators/schofield_controller/templates/nested/show.rb +0 -12
- data/generators/schofield_controller/templates/unnested/controller.rb +0 -94
- data/generators/schofield_controller/templates/unnested/edit.rb +0 -5
- data/generators/schofield_controller/templates/unnested/index.rb +0 -3
- data/generators/schofield_controller/templates/unnested/new.rb +0 -1
- data/generators/schofield_controller/templates/unnested/show.rb +0 -12
- data/generators/schofield_controller/templates/view_spec.rb +0 -12
- data/generators/schofield_form/schofield_form_generator.rb +0 -22
- data/generators/schofield_form/templates/view__form.html.haml +0 -11
- data/lib/schofield.rb +0 -111
- data/lib/schofield/tasks.rb +0 -3
- 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
|