sequel-crushyform 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Mickael Riga
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,298 @@
1
+ CRUSHYFORM
2
+ ==========
3
+
4
+ Crushyform is a Sequel plugin that helps building forms.
5
+ It basically does them for you so that you can forget about the boring part.
6
+ The kind of thing which is good to have in your toolbox for building a CMS.
7
+
8
+ We tried to make it as modular as possible but with sensible default values.
9
+
10
+ I am also aware that this documentation is new and might lack some crucial information
11
+ but feel free to drop me a line if you have any question.
12
+
13
+ HOW TO INSTALL ?
14
+ ================
15
+
16
+ Crushyform is a Ruby Gem so you can install it with:
17
+
18
+ sudo gem install sequel-crushyform
19
+
20
+ HOW TO USE ? (THE BASICS)
21
+ =========================
22
+
23
+ Crushyform is also a Sequel plugin so you can add the crushyform methods to all your models with:
24
+
25
+ ::Sequel::Model.plugin :crushyform
26
+
27
+ Or you can just add it to one model that way:
28
+
29
+ class BlogPost < ::Sequel::Model
30
+ plugin :crushyform
31
+ # More useful code from you
32
+ end
33
+
34
+ Once you have it, that is when the magic begins.
35
+ You already can have a form for it:
36
+
37
+ BlogPost.new.crushyform # Returns a nice form
38
+
39
+ This will return a form for your new model. Without any effort.
40
+ If you try with an existing entry, it works as well (wow!).
41
+ If you try with an entry that is not valid, error messages are incorporated (even more wow!).
42
+
43
+ Obviously you have a bit more control on that.
44
+ To start with, this method have arguments which are:
45
+
46
+ - List of columns (all of them by default)
47
+ - Action URL (when nil, which is the default option, the fields are not wrapped in a form tag)
48
+ - HTTP Method (POST by default)
49
+
50
+ So you can do that:
51
+
52
+ BlogPost.new.crushyform( [:title,:body], "/new_blog", "GET" ) # Returns a nice form
53
+
54
+ Which will return a form (wrapped this time) with only the 2 fields :title and :body.
55
+ Options give the action URL and method.
56
+
57
+ Now say you want to have all the default options, but you want a wrapping form tag, you can do that:
58
+
59
+ BlogPost.new.crushyform( model.crushyform_schema.keys, "/new_blog" ) # Returns a nice form (a crushy form to be precise)
60
+
61
+ You have to put the default value `model.crushyform_schema.keys` because in the order of priority, the list of columns is more important
62
+
63
+ This is due to the fact that in a real case scenario we are more likely to use the version without a wrapping form tag.
64
+ Mainly because we want to add some other hidden inputs like a destination, a method override in order to generate a PUT request or a pseudo XHR value.
65
+
66
+ Another reason might be that you want to add other fields to the tag.
67
+ The default tag is pretty basic and is always considered multipart/form-data for simplicity.
68
+
69
+ CSS CLASSES
70
+ ===========
71
+
72
+ Here is the list of CSS classes used in order to style the forms.
73
+ That should be enough, drop me a line if you feel something is missing.
74
+
75
+ - crushyfield-required is the class for the default required flag
76
+ - crushyfield is used on the wrapping paragraph tag of every fields
77
+ - crushyfield-error is used on the wrapping paragraph tag of a field containing errors
78
+ - crushyfield-error-list is on the span that wraps the list of errors (is just a span, not an html list though)
79
+
80
+ THE CRUSHYFIELDS
81
+ ================
82
+
83
+ As mentioned before, the form is more like a way to gather all the fields we're interested to have in a form.
84
+ Which means you can have more control than that.
85
+ You can have just one field at a time.
86
+ In order to do that, you have 2 methods:
87
+
88
+ - Model#crushyinput(column,options) which only gives you the input tag for the field
89
+ - Model#crushyfield(column,options) which does the same but wrapped in a paragraph tag with a label and an error list
90
+
91
+ Regarding the options, you have plenty of them.
92
+ We'll see later how we can set all of them in the crushyform schema but they all can be overriden when requesting the input tag.
93
+ Here is the list of schema-related options:
94
+
95
+ - :type is the crushyform type that is used for the field (default is :string)
96
+ - :input_name is the name used for the input tag (default is model[columnname])
97
+ - :input_value is the input value used for the input tag
98
+ - :input_type is the input type used for the input tag (default is text when applicable)
99
+ - :input_class is the class value used for the input tag
100
+ - :html_escape has to be set to false if you do not want the value to be escaped (default is true)
101
+ - :required text that says that the field is required (default is just blank). A ready-made value for that field is also available if you put `true` instead of a text. It is an asterisk with span class `crushyfield_required`
102
+
103
+ As you can see, a lot of things can be overriden at the last level.
104
+ There is another option just for Model#crushyfield that is called :name.
105
+ This is basically the name of the field in the label tag.
106
+ By default, this is the name of the column in a human readable way, which means
107
+ there are no underscore signs and foreign keys like :author_id will have the name: Author.
108
+
109
+ Here is an example:
110
+
111
+ BlogPost[4].crushyfield( :title , { :name => "Enter Title", :required => true })
112
+
113
+ This will give you a full field for the column :title with a label saying "Enter Title" and an asterisk that says the field is required.
114
+ As mentioned before the :required can be the text to put, but for consistency, it is recommanded to wrap it in the same span with the class crushyfield-required.
115
+ So if you want to simply write required:
116
+
117
+ <span class='crushyfield-required'> required</span>
118
+
119
+ But really this is good only if the text is different for that specific field.
120
+ You usually want to override the class method Model::crushyfield_required.
121
+ The default implementation is:
122
+
123
+ def self.crushyfield_required
124
+ "<span class='crushyfield-required'> *</span>"
125
+ end
126
+
127
+ TYPES OF FIELD
128
+ ==============
129
+
130
+ - :string is the default one so it is used when the field is :string type or anyone that is not in the list like :integer for instance
131
+ - :none returns a blank string
132
+ - :boolean
133
+ - :text
134
+ - :date is in the format YYYY-MM-DD because it is accepted by sequel setters as-is
135
+ - :time is in the format HH:MM:SS because it is accepted by sequel setters as-is
136
+ - :datetime is in the format YYYY-MM-DD HH:MM:SS because it is accepted by sequel setters as-is
137
+ - :parent is a dropdown list to chose from
138
+ - :attachment is for attachments (who guessed?).
139
+
140
+ MORE ABOUT DATE/TIME FIELDS
141
+ ---------------------------
142
+
143
+ As you can see date/time/datetime field is a text input with a format specified on the side.
144
+ We used to deal with it differently in the past, but nowadays this is the kind of field that is better to keep basic and offer a better interface with javascript.
145
+ Better to give it a special :input_class through the options and make it a nice javascript date picker
146
+ instead of trying to complicate something that is gonna be ugly at the end anyway.
147
+
148
+ Also if you want to use a proper time field (just time with no date), don't forget to declare it all lowercase in your schema.
149
+ Otherwise it will use the Time ruby class which is a time including the date:
150
+
151
+ set_schema do
152
+ primary_key :id
153
+ Time :opening_hour # type is :datetime
154
+ time :opening_hour # type is :time
155
+ end
156
+
157
+ MORE ABOUT ATTACHMENT FIELD
158
+ ---------------------------
159
+
160
+ Regarding the :attachment type, it should be able to work with any kind of system.
161
+ We made it simple and customizable enough to adapt to many attachment solutions.
162
+ A called it :attachment because I never really use blobs, but it might be used with blobs as well.
163
+ Once again because it is very basic.
164
+
165
+ This is typically the kind of field that cannot really be guessed by crushyform.
166
+ So you have to declare it as an :attachment.
167
+ We see how it is done in the following chapter.
168
+
169
+ Also when it can, crushyform tries to put a thumbnail of the attachment, above the file input when possible.
170
+ It is done with an instance method that can be overriden by you: Model#to_thumb( column ).
171
+ By default, it does the right job if you're using another Gem we've done called [Stash-Magic](https://github.com/mig-hub/stash_magic) .
172
+ Otherwise crushyform assumes that the column contains the relative URL of an image.
173
+
174
+ MORE ABOUT PARENT FIELDS
175
+ ------------------------
176
+
177
+ The :parent field type is quite straight foreward and there is not much to say in order to be able to use it.
178
+ It is interesting to see how it works though.
179
+ You have a dropdown with all parents name instead of just a crude ID number.
180
+ One interesting thing is that this dropdown is available for you to use for an Ajax update or whatever:
181
+
182
+ Author.to_dropdown( 3, "Choose your Author" )
183
+
184
+ Both options are optional. The first one is the ID of the author that is selected (default is `nil`).
185
+ And the second option is the text for the nil option (default is "** UNDEFINED **").
186
+
187
+ This dropdown is cached in `Model::dropdown_cache` and is automatically reseted when you create, update or destroy an entry.
188
+ Alternatively, you can do it with `Model::reset_dropdown_cache`.
189
+
190
+ Another interesting thing is the way crushyform comes up with names.
191
+ You rarely would have to do anything because it maintains an ordered list of columns that are appropriate for a name.
192
+ The current list is in a constant:
193
+
194
+ LABEL_COLUMNS = [:title, :label, :fullname, :full_name, :surname, :lastname, :last_name, :name, :firstname, :first_name, :caption, :reference, :file_name, :body]
195
+
196
+ In the worst case scenario, if it cannot find a column, crushyform will call it with the class name followed by the ID number.
197
+
198
+ Alternatively, you can specify your own column:
199
+
200
+ Author.label_column = :my_label_column
201
+
202
+ Or you can override the final instance method `Model::to_label`:
203
+
204
+ def to_label
205
+ self.my_label_column
206
+ end
207
+
208
+ The good thing that this method is very useful in many places of CMS of your application, and even the front end:
209
+
210
+ @author.to_label
211
+
212
+ It could even work with addresses.
213
+ Crushyform turns a multi-line text in a one liner if it is the label column.
214
+
215
+ # Say the class Address has a column called :body which is the best choice for a label
216
+ #
217
+ # 4, Virginia Street
218
+ # Flat C
219
+
220
+ @address.to_label # => 4, Virginia Street Flat C
221
+
222
+ You get the idea.
223
+
224
+ CRUSHYFORM SCHEMA
225
+ =================
226
+
227
+ So now some people might think that this is weird that you have to put the options each time you ask for a field.
228
+ Well, the good news is that you don't.
229
+ Most of the options on Model#crushyfield are just for specific cases where you want to override/force something.
230
+ Instead you have a crushyform_schema:
231
+
232
+ BlogPost.crushyform_schema
233
+ # returns something like:
234
+ # {
235
+ # :title => { :type => :string },
236
+ # :body => { :type => :text },
237
+ # :created_on => { :type => :date },
238
+ # :picture => { :type => :string }
239
+ # }
240
+
241
+ As you can see, it is already filled for you with the bare minimum.
242
+ Unfortunately the picture is a string, but a string that is really an image URL.
243
+ You can fix that with:
244
+
245
+ BlogPost.crushyform_schema[:picture].update({ :type => :attachment })
246
+
247
+ Alright now when you ask for the form or just the :picture field, it is gonna be an file upload field.
248
+ But you also want the :title to be displayed as a mandatory field:
249
+
250
+ BlogPost.crushyform_schema[:title].update({ :required => true })
251
+
252
+ You get the idea.
253
+
254
+ But there is even a better way if you use the :schema plugin at the same time.
255
+ You can add an option called :crushyform for each column.
256
+ For instance, in order to do the same thing as before:
257
+
258
+ class BlogPost < ::Sequel::Model
259
+ plugin :schema
260
+ plugin :crushyform
261
+ set_schema do
262
+ primary_key :id
263
+ String :title, :crushyform => {:required => true}
264
+ text :body
265
+ Date :created_on
266
+ String :picture, :crushyform => {:type => :attachment}
267
+ end
268
+ create_table unless table_exists?
269
+ end
270
+
271
+ CUSTOM TYPE OF FIELD
272
+ ====================
273
+
274
+ You can obviously create a type of field that is not implemented in crushyform.
275
+ If this is a useful one, it is probably better to fork the project on Github and send me a pull request.
276
+ That way, you'll help crushyform being more interesting.
277
+
278
+ Otherwise, the list of types is a Hash. Key is the name, and the value is a Proc with a couple of arguments.
279
+ A dummy example could be:
280
+
281
+ Author.crushyform_types.update({
282
+ :dummy_type => proc do |instance, column_name, options|
283
+ "<p>You cannot change column: #{column_name}</p>"
284
+ end
285
+ })
286
+
287
+ So it returns a string.
288
+ Pretty simple.
289
+
290
+ CHANGE LOG
291
+ ==========
292
+
293
+ 0.0.1 First version
294
+
295
+ COPYRIGHT
296
+ =========
297
+
298
+ (c) 2011 Mickael Riga - see file LICENCE for details
@@ -0,0 +1,145 @@
1
+ module ::Sequel::Plugins::Crushyform
2
+
3
+ module ClassMethods
4
+ def crushyform_version; [0,0,1]; end
5
+ # Schema
6
+ def crushyform_schema
7
+ @crushyform_schema ||= default_crushyform_schema
8
+ end
9
+ def default_crushyform_schema
10
+ out = {}
11
+ db_schema.each do |k,v|
12
+ out[k] = if v[:db_type]=='text'
13
+ {:type=>:text}
14
+ else
15
+ {:type=>v[:type]}
16
+ end
17
+ end
18
+ @schema.columns.each{|c|out[c[:name]]=out[c[:name]].update(c[:crushyform]) if c.has_key?(:crushyform)} if respond_to?(:schema)
19
+ association_reflections.each{|k,v|out[v[:key]]={:type=>:parent} if v[:type]==:many_to_one}
20
+ out
21
+ end
22
+ # Types
23
+ def crushyform_types
24
+ @crushyform_types ||= {
25
+ :none => proc{''},
26
+ :string => proc do |m,c,o|
27
+ "<input type='%s' name='%s' value='%s' id='%s' class='%s' />%s\n" % [o[:input_type]||'text', o[:input_name], o[:input_value], m.crushyid_for(c), o[:input_class], o[:required]]
28
+ end,
29
+ :boolean => proc do |m,c,o|
30
+ crushid = m.crushyid_for(c)
31
+ s = ['checked', nil]
32
+ s.reverse! unless o[:input_value]
33
+ out = "<span class='%s'>"
34
+ out += "<input type='radio' name='%s' value='true' id='%s' %s /> <label for='%s'>Yes</label> "
35
+ out += "<input type='radio' name='%s' value='false' id='%s-no' %s /> <label for='%s-no'>No</label>"
36
+ out += "</span>\n"
37
+ out % [o[:input_class], o[:input_name], crushid, s[0], crushid, o[:input_name], crushid, s[1], crushid]
38
+ end,
39
+ :text => proc do |m,c,o|
40
+ "<textarea name='%s' id='%s' class='%s'>%s</textarea>%s\n" % [o[:input_name], m.crushyid_for(c), o[:input_class], o[:input_value], o[:required]]
41
+ end,
42
+ :date => proc do |m,c,o|
43
+ o[:input_value] = "%s-%s-%s" % [o[:input_value].year, o[:input_value].month, o[:input_value].day] if o[:input_value].is_a?(Sequel.datetime_class)
44
+ o[:required] = "%s Format: yyyy-mm-dd" % [o[:required]]
45
+ crushyform_types[:string].call(m,c,o)
46
+ end,
47
+ :time => proc do |m,c,o|
48
+ o[:input_value] = "%s:%s:%s" % [o[:input_value].hour, o[:input_value].min, o[:input_value].sec] if o[:input_value].is_a?(Sequel.datetime_class)
49
+ o[:required] = "%s Format: hh:mm:ss" % [o[:required]]
50
+ crushyform_types[:string].call(m,c,o)
51
+ end,
52
+ :datetime => proc do |m,c,o|
53
+ o[:input_value] = "%s-%s-%s %s:%s:%s" % [o[:input_value].year, o[:input_value].month, o[:input_value].day, o[:input_value].hour, o[:input_value].min, o[:input_value].sec] if o[:input_value].is_a?(Sequel.datetime_class)
54
+ o[:required] = "%s Format: yyyy-mm-dd hh:mm:ss" % [o[:required]]
55
+ crushyform_types[:string].call(m,c,o)
56
+ end,
57
+ :parent => proc do |m,c,o|
58
+ parent_class = association_reflection(c.to_s.sub(/_id$/,'').to_sym).associated_class
59
+ option_list = parent_class.to_dropdown(o[:input_value])
60
+ "<select name='%s' id='%s' class='%s'>%s</select>\n" % [o[:input_name], m.crushyid_for(c), o[:input_class], option_list]
61
+ end,
62
+ :attachment => proc do |m,c,o|
63
+ "%s<input type='file' name='%s' id='%s' class='%s' />%s\n" % [m.to_thumb(c), o[:input_name], m.crushyid_for(c), o[:input_class], o[:required]]
64
+ end
65
+ }
66
+ end
67
+ # What represents a required field
68
+ # Can be overriden
69
+ def crushyfield_required; "<span class='crushyfield-required'> *</span>"; end
70
+ # Stolen from ERB
71
+ def html_escape(s)
72
+ s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
73
+ end
74
+ # Cache dropdown options for children classes to use
75
+ # Meant to be reseted each time an entry is created, updated or destroyed
76
+ # So it is only rebuild once required after the list has changed
77
+ # Maintaining an array and not rebuilding it all might be faster
78
+ # But it will not happen much so that it is fairly acceptable
79
+ def to_dropdown(selection=nil, nil_name='** UNDEFINED **')
80
+ dropdown_cache.inject("<option value=''>#{nil_name}</option>\n") do |out, row|
81
+ selected = 'selected' if row[0]==selection
82
+ "%s%s%s%s" % [out, row[1], selected, row[2]]
83
+ end
84
+ end
85
+ def dropdown_cache
86
+ @dropdown_cache ||= label_dataset.inject([]) do |out,row|
87
+ out.push([row.id, "<option value='#{row.id}' ", ">#{row.to_label}</option>\n"])
88
+ end
89
+ end
90
+ def reset_dropdown_cache; @dropdown_cache = nil; end
91
+ # Generic column names for label
92
+ LABEL_COLUMNS = [:title, :label, :fullname, :full_name, :surname, :lastname, :last_name, :name, :firstname, :first_name, :caption, :reference, :file_name, :body]
93
+ # Column used as a label
94
+ def label_column; @label_column ||= LABEL_COLUMNS.find{|c|columns.include?(c)}; end
95
+ def label_column=(n); @label_column=n; end
96
+ # Dataset selecting only columns used for building names
97
+ def label_dataset; select(:id, label_column); end
98
+ end
99
+
100
+ module InstanceMethods
101
+ def crushyform(columns=model.crushyform_schema.keys, action=nil, meth='POST')
102
+ fields = columns.inject(""){|out,c|out+crushyfield(c.to_sym)}
103
+ action.nil? ? fields : "<form action='%s' method='%s' enctype='multipart/form-data'>%s</form>\n" % [action, meth, fields]
104
+ end
105
+ # crushyfield is crushyinput but with label+error
106
+ def crushyfield(col, o={})
107
+ field_name = o[:name] || col.to_s.sub(/_id$/, '').tr('_', ' ').capitalize
108
+ error_list = errors.on(col).map{|e|" - #{e}"} if !errors.on(col).nil?
109
+ "<p class='crushyfield %s'><label for='%s'>%s</label><span class='crushyfield-error-list'>%s</span><br />\n%s</p>\n" % [error_list&&'crushyfield-error', crushyid_for(col), field_name, error_list, crushyinput(col, o)]
110
+ end
111
+ def crushyinput(col, o={})
112
+ o = model.crushyform_schema[col].dup.update(o)
113
+ o[:input_name] ||= "model[#{col}]"
114
+ o[:input_value] = o[:input_value].nil? ? self.__send__(col) : o[:input_value]
115
+ o[:input_value] = model.html_escape(o[:input_value]) if (o[:input_value].is_a?(String) && o[:html_escape]!=false)
116
+ o[:required] = o[:required]==true ? model.crushyfield_required : o[:required]
117
+ crushyform_type = model.crushyform_types[o[:type]] || model.crushyform_types[:string]
118
+ crushyform_type.call(self,col,o)
119
+ end
120
+ # This ID is used to have a unique reference for the input field.
121
+ #
122
+ # Format: <id>-<class>-<column>
123
+ #
124
+ # If you plan to have more than one form for a new entry in the same page
125
+ # you'll have to override this method because records without an id
126
+ # have just 'new' as a prefix.
127
+ # Which means there could be a colision.
128
+ def crushyid_for(col); "%s-%s-%s" % [id||'new',self.class.name,col]; end
129
+ # Used to determine a humanly readable representation of the entry on one line of text
130
+ def to_label; model.label_column.nil? ? "#{model} #{id}" : self.__send__(model.label_column).to_s.tr("\n\r", ' '); end
131
+ # Provide a thumbnail for the column
132
+ def to_thumb(c)
133
+ current = self.__send__(c)
134
+ if model.respond_to?(:stash_reflection) && model.stash_reflection.key?(c)
135
+ !current.nil? && current[:type][/^image\//] ? "<img src='#{file_url(c, 'stash_thumb.gif')}?#{::Time.now.to_i.to_s}' /><br />\n" : ''
136
+ else
137
+ "<img src='#{current}?#{::Time.now.to_i.to_s}' width='100' onerror=\"this.style.display='none'\" />\n"
138
+ end
139
+ end
140
+ # Reset dropdowns on hooks
141
+ def after_save; model.reset_dropdown_cache; super; end
142
+ def after_destroy; model.reset_dropdown_cache; super; end
143
+ end
144
+
145
+ end
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'sequel-crushyform'
3
+ s.version = "0.0.1"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "A Sequel plugin that helps building forms"
6
+ s.description = "A Sequel plugin that helps building forms. It basically does them for you so that you can forget about the boring part. The kind of thing which is good to have in your toolbox for building a CMS."
7
+ s.files = `git ls-files`.split("\n").sort
8
+ s.require_path = './lib'
9
+ s.author = "Mickael Riga"
10
+ s.email = "mig@mypeplum.com"
11
+ s.homepage = "http://github.com/mig-hub/sequel-crushyform"
12
+ end
@@ -0,0 +1,360 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ Bacon.summary_on_exit
4
+
5
+ F = ::File
6
+ D = ::Dir
7
+ ROOT = F.dirname(__FILE__)+'/..'
8
+ $:.unshift(ROOT+'/lib')
9
+
10
+ require 'sequel'
11
+ ::Sequel::Model.plugin :crushyform
12
+ DB = ::Sequel.sqlite
13
+
14
+ class Haiku < ::Sequel::Model
15
+ plugin :schema
16
+ set_schema do
17
+ primary_key :id
18
+ String :title, :crushyform=>{:type=>:custom}
19
+ text :body
20
+ Boolean :published
21
+ foreign_key :author_id, :authors
22
+ end
23
+ create_table unless table_exists?
24
+ many_to_one :author
25
+ one_to_many :reviews
26
+ def validate
27
+ errors[:title] << "is not good"
28
+ errors[:title] << "smells like shit"
29
+ end
30
+ end
31
+
32
+ class Author < ::Sequel::Model
33
+ plugin :schema
34
+ set_schema do
35
+ primary_key :id
36
+ String :name
37
+ String :surname
38
+ end
39
+ create_table unless table_exists?
40
+ one_to_many :haikus
41
+ end
42
+ Author.create(:name=>'Ray',:surname=>'Bradbury')
43
+ Author.create(:name=>'Jorge Luis',:surname=>'Borges')
44
+
45
+ DB.create_table :reviews do
46
+ primary_key :id
47
+ String :title
48
+ text :body
49
+ Integer :rate
50
+ foreign_key :haiku_id, :haikus
51
+ end
52
+ class Review < ::Sequel::Model
53
+ many_to_one :haiku
54
+ end
55
+
56
+ class TestDateTime < ::Sequel::Model
57
+ plugin :schema
58
+ set_schema do
59
+ primary_key :id
60
+ Date :birth
61
+ time :meeting
62
+ DateTime :when
63
+ DateTime :created_at
64
+ DateTime :updated_at
65
+ end
66
+ create_table unless table_exists?
67
+ end
68
+
69
+ class ShippingAddress < ::Sequel::Model
70
+ plugin :schema
71
+ set_schema do
72
+ primary_key :id
73
+ text :address_body
74
+ String :postcode
75
+ String :city
76
+ end
77
+ create_table unless table_exists?
78
+ end
79
+ ShippingAddress.create(:address_body=>"3 Mulholland Drive\n\rFlat C", :postcode=>'90210', :city=>'Richville')
80
+
81
+ require 'stash_magic'
82
+ class Attached < ::Sequel::Model
83
+ plugin :schema
84
+ set_schema do
85
+ primary_key :id
86
+ String :filename, :crushyform=>{:type=>:attachment}
87
+ String :filesize, :crushyform=>{:type=>:none}
88
+ String :filetype, :crushyform=>{:type=>:none}
89
+ String :map, :crushyform=>{:type=>:attachment}
90
+ end
91
+ create_table unless table_exists?
92
+ ::StashMagic.with_public_root ROOT+'/test'
93
+ end
94
+
95
+ # ========
96
+ # = Test =
97
+ # ========
98
+
99
+ describe 'Crushyform when schema plugin is not used' do
100
+
101
+ should 'have a correct default crushyform_schema' do
102
+ Review.default_crushyform_schema.should=={
103
+ :id => {:type=>:integer},
104
+ :title => {:type=>:string},
105
+ :body => {:type=>:text},
106
+ :rate => {:type=>:integer},
107
+ :haiku_id => {:type=>:parent}
108
+ }
109
+ end
110
+
111
+ should 'build the default crushyform_schema on the first query' do
112
+ Review.respond_to?(:schema).should==false
113
+ Review.crushyform_schema.should==Review.default_crushyform_schema
114
+ end
115
+
116
+ should 'have an updatable crushyform_schema' do
117
+ Review.crushyform_schema[:body][:type] = :custom
118
+ Review.crushyform_schema.should.not==Review.default_crushyform_schema
119
+ Review.crushyform_schema[:body][:type] = :text
120
+ end
121
+
122
+ end
123
+
124
+ describe 'Crushyform when schema plugin is used' do
125
+
126
+ should 'use schema declaration for building the default crushyform_schema' do
127
+ Haiku.default_crushyform_schema.should=={
128
+ :id => {:type=>:integer},
129
+ :title => {:type=>:custom},
130
+ :body => {:type=>:text},
131
+ :published => {:type=>:boolean},
132
+ :author_id => {:type=>:parent}
133
+ }
134
+ end
135
+
136
+ end
137
+
138
+ describe 'Crushyform miscellaneous helpers' do
139
+
140
+ should 'know its crushyform version' do
141
+ Haiku.crushyform_version.size.should==3
142
+ end
143
+
144
+ should 'have a correct default crushyid' do
145
+ ShippingAddress.new.crushyid_for(:address_body).should=='new-ShippingAddress-address_body'
146
+ ShippingAddress.first.crushyid_for(:address_body).should=='1-ShippingAddress-address_body'
147
+ end
148
+
149
+ should 'not mark texts as type :string, but :text' do
150
+ Haiku.db_schema[:body][:type].should==:string
151
+ Haiku.crushyform_schema[:body][:type].should==:text
152
+ end
153
+
154
+ should 'guess label columns using a list of common column names' do
155
+ Haiku.label_column.should==:title
156
+ Author.label_column.should==:surname # Respect order of search
157
+ end
158
+
159
+ should 'set Model::label_column' do
160
+ TestDateTime.label_column.should==nil
161
+ TestDateTime.label_column = :birth
162
+ TestDateTime.label_column.should==:birth
163
+ TestDateTime.label_column = nil
164
+ end
165
+
166
+ should 'have a shortcut for dataset only with columns relevant for building a dropdown' do
167
+ a = Author.label_dataset.first
168
+ a.surname.should=='Bradbury'
169
+ a.name.should==nil
170
+ end
171
+
172
+ should 'have a label based on Model::label_column' do
173
+ Author.first.to_label.should=='Bradbury'
174
+ end
175
+
176
+ should 'Have a fallback label when label_column is nil' do
177
+ ShippingAddress.first.to_label.should=="ShippingAddress 1"
178
+ end
179
+
180
+ should 'avoid line breaks if label column is a multiline field' do
181
+ ShippingAddress.label_column = :address_body
182
+ ShippingAddress.first.to_label.should=="3 Mulholland Drive Flat C"
183
+ end
184
+
185
+ should 'build correct dropdowns' do
186
+ options = Author.to_dropdown(1)
187
+ options.lines.count.should==3
188
+ options.should.match(/<option[^>]+value='1'[^>]+selected>Bradbury<\/option>/)
189
+ options = Author.to_dropdown
190
+ options.should.not.match(/selected/)
191
+ end
192
+
193
+ should 'have custom wording for nil value for parent dropdowns' do
194
+ options = Author.to_dropdown(1,"Pick an Author")
195
+ options.should.match(/^<option value=''>Pick an Author<\/option>/)
196
+ end
197
+
198
+ should 'cache parent dropdowns' do
199
+ Author.insert(:name=>'Matsuo', :surname=>'Basho') # insert or delete do not trigger hooks
200
+ Author.to_dropdown(1).lines.count.should==3
201
+ Author.reset_dropdown_cache
202
+ Author.to_dropdown(1).lines.count.should==4
203
+ Author.order(:id).last.delete
204
+ end
205
+
206
+ should 'have parent dropdown cache reseted when list is changed' do
207
+ a = Author.create(:name=>'Yasunari', :surname=>'Kawabati')
208
+ Author.to_dropdown(1).lines.count.should==4
209
+ a.update(:surname=>'Kawabata')
210
+ Author.to_dropdown(1).should.match(/Kawabata/)
211
+ a.destroy
212
+ Author.to_dropdown(1).lines.count.should==3
213
+ end
214
+
215
+ should 'have a generic entry point overridable for grabbing thumbnails' do
216
+ Attached.new.respond_to?(:to_thumb).should==true
217
+ end
218
+
219
+ should 'have a thumbnail by default that use the content of column as path and invisible if path is broken' do
220
+ a = Attached.new.to_thumb(:filename)
221
+ a.should.match(/^<img.*\/>$/)
222
+ a.should.match(/src='\?\d*'/)
223
+ a.should.match(/onerror=\"this.style.display='none'\"/)
224
+ a = Attached.new.set(:filename=>'/book.png').to_thumb(:filename)
225
+ a.should.match(/^<img.*\/>$/)
226
+ a.should.match(/src='\/book\.png\?\d*'/)
227
+ a.should.match(/onerror=\"this.style.display='none'\"/)
228
+ end
229
+
230
+ should 'have a special thumbnail behavior adapted to StashMagic if that Gem is used on the specific field' do
231
+ a = Attached.new.set(:map=>"{:type=>'image/png',:name=>'map.png',:size=>20}")
232
+ b = Attached.new.set(:map=>"{:type=>'application/pdf',:name=>'map.pdf',:size=>20}")
233
+ Attached.stash :map # I do it only here so that I could enter test values as a simple string
234
+ field = a.to_thumb(:map)
235
+ field.should.match(/^<img.*\/><br \/>$/)
236
+ field.should.match(/src='\/stash\/Attached\/tmp\/map\.stash_thumb\.gif\?\d*'/)
237
+ field.should.not.match(/onerror=\"this.style.display='none'\"/)
238
+ # No preview if field is nil or not an image
239
+ a.map = nil
240
+ a.to_thumb(:map).should==''
241
+ b.to_thumb(:map).should==''
242
+ end
243
+ end
244
+
245
+ describe 'Crushyfield types' do
246
+
247
+ should 'have a type that does nothing' do
248
+ Attached.new.crushyinput(:filesize).should==''
249
+ end
250
+
251
+ should 'escape html by default on text fields' do
252
+ Haiku.new.crushyinput(:title, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/&lt;ScRipT &gt;alert\('test'\);&lt;\/ScRipT &gt;/)
253
+ Haiku.new.crushyinput(:body, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/&lt;ScRipT &gt;alert\('test'\);&lt;\/ScRipT &gt;/)
254
+ TestDateTime.new.crushyinput(:birth, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/&lt;ScRipT &gt;alert\('test'\);&lt;\/ScRipT &gt;/)
255
+ TestDateTime.new.crushyinput(:meeting, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/&lt;ScRipT &gt;alert\('test'\);&lt;\/ScRipT &gt;/)
256
+ TestDateTime.new.crushyinput(:when, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/&lt;ScRipT &gt;alert\('test'\);&lt;\/ScRipT &gt;/)
257
+ end
258
+
259
+ should 'not escape html on text field if specified' do
260
+ Haiku.new.crushyinput(:title, {:input_value=>"<ScRipT >alert('test');</ScRipT >", :html_escape => false}).should.should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
261
+ Haiku.new.crushyinput(:body, {:input_value=>"<ScRipT >alert('test');</ScRipT >", :html_escape => false}).should.should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
262
+ end
263
+
264
+ should 'not keep one-shot vars like :input_value in the crushyform_schema' do
265
+ Haiku.crushyform_schema[:title][:input_value].should==nil
266
+ end
267
+
268
+ should 'be able to turn the :string input into other similar types like password or hidden' do
269
+ Haiku.new.crushyinput(:title, {:input_type=>'password'}).should.match(/type='password'/)
270
+ end
271
+
272
+ should 'set booleans correctly' do
273
+ Haiku.new.published.should==nil
274
+ Haiku.new.crushyinput(:published).should.match(/<input[^>]+value='false'[^>]+checked \/>/)
275
+ Haiku.new.crushyinput(:published,{:input_value=>true}).should.match(/<input[^>]+value='true'[^>]+checked \/>/)
276
+ Haiku.new.crushyinput(:published,{:input_value=>false}).should.match(/<input[^>]+value='false'[^>]+checked \/>/)
277
+ end
278
+
279
+ should 'have :required option which is a text representing requirement and defaulting to blank' do
280
+ Review.new.crushyinput(:title).should.not.match(/#{Regexp.escape Review.crushyfield_required}/)
281
+ Review.new.crushyinput(:title,{:required=>" required"}).should.match(/required/)
282
+ end
283
+
284
+ should 'use the default requirement text when :required option is true instead of a string' do
285
+ Review.new.crushyinput(:title,{:required=>true}).should.match(/#{Regexp.escape Review.crushyfield_required}/)
286
+ end
287
+
288
+ should 'format date/time/datetime correctly' do
289
+ TestDateTime.new.db_schema[:meeting][:type].should== :time # Check that the correct type is used for following tests (see README)
290
+ TestDateTime.new.crushyinput(:birth).should.match(/value=''/)
291
+ TestDateTime.new.crushyinput(:birth,{:input_value=>::Time.now}).should.match(/value='\d{4}-\d{1,2}-\d{1,2}'/)
292
+ TestDateTime.new.crushyinput(:meeting).should.match(/value=''/)
293
+ TestDateTime.new.crushyinput(:meeting,{:input_value=>::Time.now}).should.match(/value='\d{1,2}:\d{1,2}:\d{1,2}'/)
294
+ TestDateTime.new.crushyinput(:when).should.match(/value=''/)
295
+ TestDateTime.new.crushyinput(:when,{:input_value=>::Time.now}).should.match(/value='\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}'/)
296
+ end
297
+
298
+ should 'add format instructions for date/time/datetime after :required bit' do
299
+ TestDateTime.new.crushyinput(:birth,{:required=>true}).should.match(/#{Regexp.escape Review.crushyfield_required} Format: yyyy-mm-dd/)
300
+ TestDateTime.new.crushyinput(:meeting,{:required=>true}).should.match(/#{Regexp.escape Review.crushyfield_required} Format: hh:mm:ss/)
301
+ TestDateTime.new.crushyinput(:when,{:required=>true}).should.match(/#{Regexp.escape Review.crushyfield_required} Format: yyyy-mm-dd hh:mm:ss/)
302
+ end
303
+
304
+ should 'build parent field with a wrapped version of parent_model#to_dropdown' do
305
+ Haiku.new.crushyinput(:author_id).should.match(/^<select.*>#{Regexp.escape Author.to_dropdown}<\/select>$/)
306
+ end
307
+
308
+ should 'display a preview with an attachment field whenever it is possible' do
309
+ a = Attached.new.set(:filename=>'/book.png')
310
+ a.crushyinput(:filename).should.match(/^#{Regexp.escape a.to_thumb(:filename)}<input type='file'.*\/>\n$/)
311
+ end
312
+
313
+ should 'have field wrapped with a correct label' do
314
+ ShippingAddress.new.crushyfield(:address_body).should.match(/<label for='#{Regexp.escape ShippingAddress.new.crushyid_for(:address_body)}'>Address body<\/label>/)
315
+ ShippingAddress.first.crushyfield(:address_body).should.match(/<label for='#{Regexp.escape ShippingAddress.first.crushyid_for(:address_body)}'>Address body<\/label>/)
316
+ ShippingAddress.new.crushyfield(:address_body,{:name=>'Address Lines'}).should.match(/<label for='#{Regexp.escape ShippingAddress.new.crushyid_for(:address_body)}'>Address Lines<\/label>/)
317
+ end
318
+
319
+ should 'have errors reported for fields' do
320
+ h = Haiku.new
321
+ h.valid?.should==false
322
+ h.crushyfield(:title).should.match(/<span class='crushyfield-error-list'> - is not good - smells like shit<\/span>/)
323
+ h.crushyfield(:title).should.match(/^<p class='crushyfield crushyfield-error'/)
324
+ h.crushyfield(:body).should.match(/<span class='crushyfield-error-list'><\/span>/)
325
+ h.crushyfield(:body).should.match(/^<p class='crushyfield '/)
326
+ # Not validated
327
+ Haiku.new.crushyfield(:title).should.match(/<span class='crushyfield-error-list'><\/span>/)
328
+ Haiku.new.crushyfield(:title).should.match(/^<p class='crushyfield '/)
329
+ end
330
+
331
+ end
332
+
333
+ describe 'Crushyform' do
334
+
335
+ should 'have a helper for creating the whole form with all current values (no per-field customization)' do
336
+ form = Haiku.new.crushyform([:title,:body,:author_id], '/receive_haiku', 'POST')
337
+ form.should.match(/action='\/receive_haiku'/)
338
+ form.should.match(/method='POST'/)
339
+ Haiku.new.crushyform([:title,:body,:author_id], '/receive_haiku').should==form # default to POST
340
+ Haiku.new.crushyform(['title','body','author_id'], '/receive_haiku').should==form # turn keys into symbols
341
+ end
342
+
343
+ should 'have only fields not wrapped with a form tag if action is nil' do
344
+ form = Haiku.new.crushyform([:title,:body,:author_id])
345
+ form.should.not.match(/form/)
346
+ form.should.not.match(/action/)
347
+ form.should.not.match(/enctype/)
348
+ end
349
+
350
+ should 'use crushyform_schema keys by default for the list of field to put in the form' do
351
+ form = Haiku.new.crushyform
352
+ form.should.not==''
353
+ Haiku.crushyform_schema.keys.each do |k|
354
+ form.should.match(/#{Haiku.new.crushyid_for(k)}/)
355
+ end
356
+ end
357
+
358
+ end
359
+
360
+ ::FileUtils.rm_rf(ROOT+'/test/stash') if F.exists?(ROOT+'/test/stash')
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-crushyform
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Mickael Riga
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-17 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: A Sequel plugin that helps building forms. It basically does them for you so that you can forget about the boring part. The kind of thing which is good to have in your toolbox for building a CMS.
23
+ email: mig@mypeplum.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - LICENSE
32
+ - README.md
33
+ - lib/sequel_crushyform.rb
34
+ - sequel-crushyform.gemspec
35
+ - test/spec_crushyform.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/mig-hub/sequel-crushyform
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - ./lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ hash: 3
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.4.2
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: A Sequel plugin that helps building forms
70
+ test_files: []
71
+