sequel-crushyform 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +298 -0
- data/lib/sequel_crushyform.rb +145 -0
- data/sequel-crushyform.gemspec +12 -0
- data/test/spec_crushyform.rb +360 -0
- metadata +71 -0
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(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
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(/<ScRipT >alert\('test'\);<\/ScRipT >/)
|
253
|
+
Haiku.new.crushyinput(:body, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
|
254
|
+
TestDateTime.new.crushyinput(:birth, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
|
255
|
+
TestDateTime.new.crushyinput(:meeting, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
|
256
|
+
TestDateTime.new.crushyinput(:when, {:input_value=>"<ScRipT >alert('test');</ScRipT >"}).should.match(/<ScRipT >alert\('test'\);<\/ScRipT >/)
|
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
|
+
|