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