campbellhay-mongo 1.0.14
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/.gitignore +3 -0
- data/Gemfile +2 -0
- data/campbellhay-mongo.gemspec +14 -0
- data/gempush +7 -0
- data/lib/mongo_crushyform.rb +221 -0
- data/lib/mongo_mutation.rb +272 -0
- data/lib/mongo_stash.rb +155 -0
- data/test/spec_mutation.rb +161 -0
- metadata +84 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'campbellhay-mongo'
|
3
|
+
s.version = "1.0.14"
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.summary = "The CampbellHay Mongo toolbox"
|
6
|
+
s.description = "The CampbellHay Mongo toolbox"
|
7
|
+
s.files = `git ls-files`.split("\n").sort
|
8
|
+
s.require_path = './lib'
|
9
|
+
s.author = "Mickael Riga"
|
10
|
+
s.email = "mig@campbellhay.com"
|
11
|
+
s.homepage = "http://www.campbellhay.com"
|
12
|
+
s.add_dependency('mongo', '>=1.8.2')
|
13
|
+
s.add_development_dependency('bacon')
|
14
|
+
end
|
data/gempush
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'mongo_mutation'
|
4
|
+
|
5
|
+
module MongoCrushyform
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def crushyform_types
|
10
|
+
@crushyform_types ||= {
|
11
|
+
:none => proc{''},
|
12
|
+
:string => proc do |m,c,o|
|
13
|
+
if o[:autocompleted]
|
14
|
+
values = o[:autocomplete_options] || m.class.collection.distinct(c)
|
15
|
+
js = <<-EOJS
|
16
|
+
<script type="text/javascript" charset="utf-8">
|
17
|
+
$(function(){
|
18
|
+
$( "##{m.field_id_for(c)}" ).autocomplete({source: ["#{values.join('","')}"]});
|
19
|
+
});
|
20
|
+
</script>
|
21
|
+
EOJS
|
22
|
+
end
|
23
|
+
tag = "<input type='%s' name='%s' value=\"%s\" id='%s' class='%s' %s />%s\n" % [o[:input_type]||'text', o[:input_name], o[:input_value], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:required]]
|
24
|
+
"#{tag}#{js}"
|
25
|
+
end,
|
26
|
+
:slug => proc do |m,c,o|
|
27
|
+
crushyform_types[:string].call(m,c,o)
|
28
|
+
end,
|
29
|
+
:boolean => proc do |m,c,o|
|
30
|
+
crushid = m.field_id_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>%s</textarea>%s\n" % [o[:input_name], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:input_value], o[:required]]
|
41
|
+
end,
|
42
|
+
:date => proc do |m,c,o|
|
43
|
+
o[:input_value] = o[:input_value].strftime("%Y-%m-%d") if o[:input_value].respond_to?(:strftime)
|
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] = o[:input_value].strftime("%T") if o[:input_value].respond_to?(:strftime)
|
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] = o[:input_value].strftime("%Y-%m-%d %T") if o[:input_value].respond_to?(:strftime)
|
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 = o[:parent_class].nil? ? Kernel.const_get(c.sub(/^id_/, '')) : m.resolve_class(o[:parent_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.field_id_for(c), o[:input_class], option_list]
|
61
|
+
end,
|
62
|
+
:children => proc do |m,c,o|
|
63
|
+
children_class = o[:children_class].nil? ? Kernel.const_get(c.sub(/^ids_/, '')) : m.resolve_class(o[:children_class])
|
64
|
+
opts = o.update({
|
65
|
+
:multiple=>true,
|
66
|
+
:select_options=>children_class.dropdown_cache
|
67
|
+
})
|
68
|
+
@crushyform_types[:select].call(m,c,opts)
|
69
|
+
end,
|
70
|
+
:attachment => proc do |m,c,o|
|
71
|
+
deleter = "<input type='checkbox' name='#{o[:input_name]}' class='deleter' value='nil' /> Delete this file<br />" unless m.doc[c].nil?
|
72
|
+
"%s%s<input type='file' name='%s' id='%s' class='%s' />%s\n" % [m.to_thumb(c), deleter, o[:input_name], m.field_id_for(c), o[:input_class], o[:required]]
|
73
|
+
end,
|
74
|
+
:select => proc do |m,c,o|
|
75
|
+
out = "<select name='%s%s' id='%s' class='%s' %s title='-- Select --'>\n" % [o[:input_name], ('[]' if o[:multiple]), m.field_id_for(c), o[:input_class], ('multiple' if o[:multiple])]
|
76
|
+
o[:select_options] = m.__send__(o[:select_options]) unless o[:select_options].kind_of?(Array)
|
77
|
+
select_options = o[:select_options].dup
|
78
|
+
if (o[:multiple] && !o[:input_value].nil? && o[:input_value].size>1)
|
79
|
+
o[:input_value].reverse.each do |v|
|
80
|
+
elem = select_options.find{|x| x==v||(x||[])[1]==v }
|
81
|
+
select_options.unshift(select_options.delete(elem)) unless elem.nil?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
if select_options.kind_of?(Array)
|
85
|
+
select_options.each do |op|
|
86
|
+
key,val = op.kind_of?(Array) ? [op[0],op[1]] : [op,op]
|
87
|
+
if key==:optgroup
|
88
|
+
out << "<optgroup label='%s'>\n" % [val]
|
89
|
+
elsif key==:closegroup
|
90
|
+
out << "</optgroup>\n"
|
91
|
+
else
|
92
|
+
selected = 'selected' if (val==o[:input_value] || (o[:input_value]||[]).include?(val))
|
93
|
+
out << "<option value='%s' %s>%s</option>\n" % [val,selected,key]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
out << "</select>%s\n" % [o[:required]]
|
98
|
+
end,
|
99
|
+
:string_list => proc do |m,c,o|
|
100
|
+
if o[:autocompleted]
|
101
|
+
values = o[:autocomplete_options] || m.class.collection.distinct(c)
|
102
|
+
js = <<-EOJS
|
103
|
+
<script type="text/javascript" charset="utf-8">
|
104
|
+
$(function(){
|
105
|
+
$( "##{m.field_id_for(c)}" )
|
106
|
+
.bind( "keydown", function( event ) {
|
107
|
+
if ( event.keyCode === $.ui.keyCode.TAB &&
|
108
|
+
$( this ).data( "autocomplete" ).menu.active ) {
|
109
|
+
event.preventDefault();
|
110
|
+
}
|
111
|
+
})
|
112
|
+
.autocomplete({
|
113
|
+
minLength: 0,
|
114
|
+
source: function( request, response ) {
|
115
|
+
response($.ui.autocomplete.filter(["#{values.join('","')}"], request.term.split(/,\s*/).pop()));
|
116
|
+
},
|
117
|
+
focus: function() { return false; },
|
118
|
+
select: function( event, ui ) {
|
119
|
+
var terms = this.value.split(/,\s*/);
|
120
|
+
terms.pop();
|
121
|
+
terms.push(ui.item.value);
|
122
|
+
terms.push("");
|
123
|
+
this.value = terms.join( ", " );
|
124
|
+
return false;
|
125
|
+
}
|
126
|
+
});
|
127
|
+
});
|
128
|
+
</script>
|
129
|
+
EOJS
|
130
|
+
o[:autocompleted] = false # reset so that it does not autocomplete for :string type below
|
131
|
+
end
|
132
|
+
tag = @crushyform_types[:string].call(m,c,o.update({:input_value=>(o[:input_value]||[]).join(',')}))
|
133
|
+
"#{tag}#{js}"
|
134
|
+
end,
|
135
|
+
:permalink => proc do |instance, column_name, options|
|
136
|
+
values = "<option value=''>Or Browse the list</option>\n"
|
137
|
+
tag = @crushyform_types[:string].call(instance, column_name, options)
|
138
|
+
return tag if options[:permalink_classes].nil?
|
139
|
+
options[:permalink_classes].each do |sym|
|
140
|
+
c = Kernel.const_get sym
|
141
|
+
entries = c.find
|
142
|
+
unless entries.count==0
|
143
|
+
values << "<optgroup label='#{c.human_name}'>\n"
|
144
|
+
entries.each do |e|
|
145
|
+
values << "<option value='#{e.permalink}' #{'selected' if e.permalink==options[:input_value]}>#{e.to_label}</option>\n"
|
146
|
+
end
|
147
|
+
values << "</optgroup>\n"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
"#{tag}<br />\n<select name='__permalink' class='permalink-dropdown'>\n#{values}</select>\n"
|
151
|
+
end
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
# What represents a required field
|
156
|
+
# Can be overriden
|
157
|
+
def crushyfield_required; "<span class='crushyfield-required'> *</span>"; end
|
158
|
+
# Stolen from ERB
|
159
|
+
def html_escape(s)
|
160
|
+
s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
161
|
+
end
|
162
|
+
# Cache dropdown options for children classes to use
|
163
|
+
# Meant to be reseted each time an entry is created, updated or destroyed
|
164
|
+
# So it is only rebuild once required after the list has changed
|
165
|
+
# Maintaining an array and not rebuilding it all might be faster
|
166
|
+
# But it will not happen much so that it is fairly acceptable
|
167
|
+
def to_dropdown(selection=nil, nil_name='** UNDEFINED **')
|
168
|
+
dropdown_cache.inject("<option value=''>#{nil_name}</option>\n") do |out, row|
|
169
|
+
selected = 'selected' if row[1]==selection
|
170
|
+
"%s%s%s%s" % [out, row[2], selected, row[3]]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
def dropdown_cache
|
174
|
+
@dropdown_cache ||= self.find({},:fields=>['_id',label_column]).inject([]) do |out,row|
|
175
|
+
out.push([row.to_label, row.id.to_s, "<option value='#{row.id}' ", ">#{row.to_label}</option>\n"])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
def reset_dropdown_cache; @dropdown_cache = nil; end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
module InstanceMethods
|
183
|
+
def crushyform(columns=model.schema.keys, action=nil, meth='POST')
|
184
|
+
columns.delete('_id')
|
185
|
+
fields = columns.inject(""){|out,c|out+crushyfield(c)}
|
186
|
+
enctype = fields.match(/type='file'/) ? "enctype='multipart/form-data'" : ''
|
187
|
+
action.nil? ? fields : "<form action='%s' method='%s' %s>%s</form>\n" % [action, meth, enctype, fields]
|
188
|
+
end
|
189
|
+
# crushyfield is crushyinput but with label+error
|
190
|
+
def crushyfield(col, o={})
|
191
|
+
return '' if (o[:type]==:none || model.schema[col][:type]==:none)
|
192
|
+
default_field_name = col[/^id_/] ? Kernel.const_get(col.sub(/^id_/, '')).human_name : col.tr('_', ' ').capitalize
|
193
|
+
field_name = o[:name] || model.schema[col][:name] || default_field_name
|
194
|
+
error_list = errors_on(col).map{|e|" - #{e}"} if !errors_on(col).nil?
|
195
|
+
invisibility = "style='display: none;'" if (o[:invisible]==true || model.schema[col][:invisible]==true)
|
196
|
+
"<p class='crushyfield %s' %s><label for='%s'>%s</label><span class='crushyfield-error-list'>%s</span><br />\n%s</p>\n" % [error_list&&'crushyfield-error', invisibility, field_id_for(col), field_name, error_list, crushyinput(col, o)]
|
197
|
+
end
|
198
|
+
def crushyinput(col, o={})
|
199
|
+
o = model.schema[col].dup.update(o)
|
200
|
+
o[:input_name] ||= "model[#{col}]"
|
201
|
+
o[:input_value] = o[:input_value].nil? ? self[col] : o[:input_value]
|
202
|
+
o[:input_value] = model.html_escape(o[:input_value]) if (o[:input_value].is_a?(String) && o[:html_escape]!=false)
|
203
|
+
o[:required] = o[:required]==true ? model.crushyfield_required : o[:required]
|
204
|
+
crushyform_type = model.crushyform_types[o[:type]] || model.crushyform_types[:string]
|
205
|
+
crushyform_type.call(self,col,o)
|
206
|
+
end
|
207
|
+
# Provide a thumbnail for the column
|
208
|
+
def to_thumb(c)
|
209
|
+
current = @doc[c]
|
210
|
+
if current.respond_to?(:[])
|
211
|
+
"<img src='/gridfs/#{@doc[c]['stash_thumb_gif']}' width='100' onerror=\"this.style.display='none'\" />\n"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
# Reset dropdowns on hooks
|
215
|
+
def after_save; model.reset_dropdown_cache; super; end
|
216
|
+
def after_delete; model.reset_dropdown_cache; super; end
|
217
|
+
# Fix types
|
218
|
+
def fix_type_string_list(k,v); @doc[k] = v.to_s.strip.split(/\s*,\s*/).compact if v.is_a?(String); end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'mongo'
|
4
|
+
|
5
|
+
module MongoMutation
|
6
|
+
|
7
|
+
def self.included(weak)
|
8
|
+
weak.extend(MutateClass)
|
9
|
+
weak.db = DB if defined?(DB)
|
10
|
+
weak.schema = BSON::OrderedHash.new
|
11
|
+
weak.relationships = BSON::OrderedHash.new
|
12
|
+
end
|
13
|
+
|
14
|
+
module MutateClass
|
15
|
+
attr_accessor :db, :schema, :relationships
|
16
|
+
attr_writer :label_column, :slug_column, :sorting_order
|
17
|
+
|
18
|
+
LABEL_COLUMNS = ['title', 'label', 'fullname', 'full_name', 'surname', 'lastname', 'last_name', 'name', 'firstname', 'first_name', 'login', 'caption', 'reference', 'file_name', 'body', '_id']
|
19
|
+
def label_column; @label_column ||= LABEL_COLUMNS.find{|c| @schema.keys.include?(c)||c=='_id'}; end
|
20
|
+
def slug_column; @slug_column ||= (@schema.find{|k,v| v[:type]==:slug}||[])[0]; end
|
21
|
+
def foreign_key_name(plural=false); "id#{'s' if plural}_"+self.name; end
|
22
|
+
def human_name; self.name.gsub(/([A-Z])/, ' \1')[1..-1]; end
|
23
|
+
def human_plural_name; human_name+'s'; end
|
24
|
+
def collection; db[self.name]; end
|
25
|
+
def ref(id)
|
26
|
+
if id.is_a?(String)&&BSON::ObjectId.legal?(id)
|
27
|
+
id = BSON::ObjectId.from_string(id)
|
28
|
+
elsif !id.is_a?(BSON::ObjectId)
|
29
|
+
id = ''
|
30
|
+
end
|
31
|
+
{'_id'=>id}
|
32
|
+
end
|
33
|
+
def find(selector={},opts={})
|
34
|
+
selector.update(opts.delete(:selector)||{})
|
35
|
+
opts = {:sort=>self.sorting_order}.update(opts)
|
36
|
+
collection.find(selector,opts).extend(CursorMutation)
|
37
|
+
end
|
38
|
+
def find_one(spec_or_object_id=nil,opts={})
|
39
|
+
spec_or_object_id.nil? ? spec_or_object_id = opts.delete(:selector) : spec_or_object_id.update(opts.delete(:selector)||{})
|
40
|
+
opts = {:sort=>self.sorting_order}.update(opts)
|
41
|
+
item = collection.find_one(spec_or_object_id,opts)
|
42
|
+
item.nil? ? nil : self.new(item)
|
43
|
+
end
|
44
|
+
def count(opts={}); collection.count(opts); end
|
45
|
+
|
46
|
+
def sorting_order
|
47
|
+
@sorting_order ||= if @schema.key?('position')&&!@schema['position'][:scope].nil?
|
48
|
+
[[@schema['position'][:scope], :asc], ['position', :asc]]
|
49
|
+
elsif @schema.key?('position')
|
50
|
+
[['position', :asc],['_id', :asc]]
|
51
|
+
else
|
52
|
+
['_id', :asc]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def sort(id_list)
|
57
|
+
id_list.each_with_index do |id, position|
|
58
|
+
collection.update(ref(id), {'$set' => {'position'=>position}})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# CRUD
|
63
|
+
def get(id, opts={}); doc = collection.find_one(ref(id), opts); doc.nil? ? nil : self.new(doc); end
|
64
|
+
def delete(id); collection.remove(ref(id)); end
|
65
|
+
|
66
|
+
def is_unique(doc={})
|
67
|
+
return unless collection.count==0
|
68
|
+
doc = {'_id'=>BSON::ObjectId('000000000000000000000000')}.update(doc)
|
69
|
+
d = self.new
|
70
|
+
d.doc.update(doc)
|
71
|
+
d.save
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def slot(name,opts={})
|
76
|
+
@schema[name] = {:type=>:string}.update(opts)
|
77
|
+
define_method(name) { @doc[name] }
|
78
|
+
define_method("#{name}=") { |x| @doc[name] = x }
|
79
|
+
end
|
80
|
+
def image_slot(name='image',opts={})
|
81
|
+
slot name, {:type=>:attachment}.update(opts)
|
82
|
+
slot "#{name}_tooltip"
|
83
|
+
slot "#{name}_alternative_text"
|
84
|
+
end
|
85
|
+
def has_many(k,opts={}); @relationships[k] = opts; end
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_accessor :doc, :old_doc, :errors, :is_new
|
89
|
+
def initialize(document=nil); @errors={}; @doc = document || default_doc; end
|
90
|
+
def default_doc
|
91
|
+
@is_new = true
|
92
|
+
out = {}
|
93
|
+
model.schema.each { |k,v| out.store(k,v[:default].is_a?(Proc) ? v[:default].call : v[:default]) }
|
94
|
+
out
|
95
|
+
end
|
96
|
+
def model; self.class; end
|
97
|
+
def id; @doc['_id']; end
|
98
|
+
def [](field); @doc[field]; end
|
99
|
+
def []=(field,val); @doc[field] = val; end
|
100
|
+
def to_label; @doc[model.label_column].to_s.tr("\n\r", ' '); end
|
101
|
+
ACCENTS_FROM = "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž"
|
102
|
+
ACCENTS_TO = "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz"
|
103
|
+
def auto_slug
|
104
|
+
s = self.to_label.tr(ACCENTS_FROM,ACCENTS_TO).tr(' .,;:?!/\'"()[]{}<>','-').gsub(/&/, 'and')
|
105
|
+
defined?(::Rack::Utils) ? ::Rack::Utils.escape(s) : s
|
106
|
+
end
|
107
|
+
def to_slug; @doc[model.column_slug]||self.auto_slug; end
|
108
|
+
def to_param; "#{@doc['_id']}-#{to_label.scan(/\w+/).join('-')}"; end
|
109
|
+
def field_id_for(col); "%s-%s-%s" % [id||'new',model.name,col]; end
|
110
|
+
|
111
|
+
# relationships
|
112
|
+
def resolve_class(k); k.kind_of?(Class) ? k : Kernel.const_get(k); end
|
113
|
+
def parent(k, opts={})
|
114
|
+
if k.kind_of?(String)
|
115
|
+
key = k
|
116
|
+
klass = resolve_class(model.schema[k][:parent_class])
|
117
|
+
else
|
118
|
+
klass = resolve_class(k)
|
119
|
+
key = klass.foreign_key_name
|
120
|
+
end
|
121
|
+
klass.get(@doc[key], opts)
|
122
|
+
end
|
123
|
+
def slot_children(k, opts={})
|
124
|
+
if k.kind_of?(String)
|
125
|
+
key = k
|
126
|
+
klass = resolve_class(model.schema[k][:children_class])
|
127
|
+
else
|
128
|
+
klass = resolve_class(k)
|
129
|
+
key = klass.foreign_key_name(true)
|
130
|
+
end
|
131
|
+
ids = (@doc[key]||[]).map{|i| BSON::ObjectId.from_string(i) }
|
132
|
+
selector = {'_id'=>{'$in'=>ids}}
|
133
|
+
sort_proc = proc{ |a,b| ids.index(a['_id'])<=>ids.index(b['_id']) }
|
134
|
+
klass.find(selector, opts).to_a.sort(&sort_proc)
|
135
|
+
end
|
136
|
+
def first_slot_child(k, opts={})
|
137
|
+
if k.kind_of?(String)
|
138
|
+
key = k
|
139
|
+
klass = resolve_class(model.schema[k][:children_class])
|
140
|
+
else
|
141
|
+
klass = resolve_class(k)
|
142
|
+
key = klass.foreign_key_name(true)
|
143
|
+
end
|
144
|
+
klass.get((@doc[key]||[])[0], opts)
|
145
|
+
end
|
146
|
+
def children(k,opts={})
|
147
|
+
k = resolve_class(k)
|
148
|
+
slot_name = opts.delete(:slot_name) || model.foreign_key_name
|
149
|
+
k.find({slot_name=>@doc['_id'].to_s}, opts)
|
150
|
+
end
|
151
|
+
def first_child(k,opts={})
|
152
|
+
k = resolve_class(k)
|
153
|
+
slot_name = opts.delete(:slot_name) || model.foreign_key_name
|
154
|
+
d = k.find_one({slot_name=>@doc['_id'].to_s}, opts)
|
155
|
+
end
|
156
|
+
def children_count(k,sel={})
|
157
|
+
k = resolve_class(k)
|
158
|
+
slot_name = sel.delete(:slot_name) || model.foreign_key_name
|
159
|
+
k.collection.count(:query => {slot_name=>@doc['_id'].to_s}.update(sel))
|
160
|
+
end
|
161
|
+
|
162
|
+
# CRUD
|
163
|
+
def delete
|
164
|
+
before_delete
|
165
|
+
model.delete(@doc['_id'])
|
166
|
+
after_delete
|
167
|
+
end
|
168
|
+
|
169
|
+
# saving and hooks
|
170
|
+
def new?; @is_new ||= !@doc.key?('_id'); end
|
171
|
+
def update_doc(fields); @old_doc = @doc.dup; @doc.update(fields); @is_new = false; self; end
|
172
|
+
# Getter and setter in one
|
173
|
+
def errors_on(col,message=nil)
|
174
|
+
message.nil? ? @errors[col] : @errors[col] = (@errors[col]||[]) << message
|
175
|
+
end
|
176
|
+
def before_delete; @old_doc = @doc.dup; end
|
177
|
+
alias before_destroy before_delete
|
178
|
+
def after_delete
|
179
|
+
model.relationships.each do |k,v|
|
180
|
+
Kernel.const_get(k).find({model.foreign_key_name=>@old_doc['_id'].to_s}).each{|m| m.delete} unless v[:independent]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
alias after_destroy after_delete
|
184
|
+
def valid?
|
185
|
+
before_validation
|
186
|
+
validate
|
187
|
+
after_validation
|
188
|
+
@errors.empty?
|
189
|
+
end
|
190
|
+
def before_validation
|
191
|
+
@errors = {}
|
192
|
+
@doc.each do |k,v|
|
193
|
+
next unless model.schema.key?(k)
|
194
|
+
type = k=='_id' ? :primary_key : model.schema[k][:type]
|
195
|
+
fix_method = "fix_type_#{type}"
|
196
|
+
if v==''
|
197
|
+
default = model.schema[k][:default]
|
198
|
+
@doc[k] = default.is_a?(Proc) ? default.call : default
|
199
|
+
else
|
200
|
+
self.__send__(fix_method, k, v) if self.respond_to?(fix_method)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
def validate; end
|
205
|
+
def after_validation; end
|
206
|
+
def fix_type_integer(k,v); @doc[k] = v.to_i; end
|
207
|
+
def fix_type_boolean(k,v); @doc[k] = (v=='true'||v==true) ? true : false; end
|
208
|
+
def fix_type_slug(k,v); @doc[k] = self.auto_slug if v.to_s==''; end
|
209
|
+
def fix_type_date(k,v)
|
210
|
+
if v.is_a?(String)
|
211
|
+
if v[/\d\d\d\d-\d\d-\d\d/]
|
212
|
+
@doc[k] = ::Time.utc(*v.split('-'))
|
213
|
+
else
|
214
|
+
default = model.schema[k][:default]
|
215
|
+
@doc[k] = default.is_a?(Proc) ? default.call : default
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
def fix_type_datetime(k,v)
|
220
|
+
if v.is_a?(String)
|
221
|
+
if v[/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/]
|
222
|
+
@doc[k] = ::Time.utc(*v.split(/[-:\s]/))
|
223
|
+
else
|
224
|
+
default = model.schema[k][:default]
|
225
|
+
@doc[k] = default.is_a?(Proc) ? default.call : default
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def save
|
231
|
+
return nil unless valid?
|
232
|
+
before_save
|
233
|
+
if new?
|
234
|
+
before_create
|
235
|
+
id = model.collection.insert(@doc)
|
236
|
+
@doc['_id'] = id
|
237
|
+
after_create
|
238
|
+
else
|
239
|
+
before_update
|
240
|
+
id = model.collection.update({'_id'=>@doc['_id']}, @doc)
|
241
|
+
after_update
|
242
|
+
end
|
243
|
+
after_save
|
244
|
+
id.nil? ? nil : self
|
245
|
+
end
|
246
|
+
def before_save; end
|
247
|
+
def before_create; end
|
248
|
+
def before_update; end
|
249
|
+
def after_save; end
|
250
|
+
def after_create; @is_new = false; end
|
251
|
+
def after_update; end
|
252
|
+
|
253
|
+
# ==========
|
254
|
+
# = Cursor =
|
255
|
+
# ==========
|
256
|
+
module CursorMutation
|
257
|
+
def self.extended(into)
|
258
|
+
into.set_mutant_class
|
259
|
+
end
|
260
|
+
def set_mutant_class
|
261
|
+
@mutant_class = Kernel.const_get(collection.name)
|
262
|
+
end
|
263
|
+
def next
|
264
|
+
n = super
|
265
|
+
n.nil? ? nil : @mutant_class.new(n)
|
266
|
+
end
|
267
|
+
# legacy
|
268
|
+
def each_mutant(&b); each(&b); end
|
269
|
+
def each_mutant_with_index(&b); each_with_index(&b); end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
data/lib/mongo_stash.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'mongo_mutation'
|
4
|
+
|
5
|
+
module MongoStash
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
MongoStash.classes << base
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
base.gridfs = GRID
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
attr_accessor :gridfs
|
15
|
+
def all_after_stash
|
16
|
+
self.collection.find.each do |i|
|
17
|
+
self.schema.each do |k,v|
|
18
|
+
obj = self.new(i)
|
19
|
+
obj.after_stash(k) if v[:type]==:attachment&&obj[k].to_s!=''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_image_tag(col='image', style='original', html_attributes={})
|
26
|
+
return '' if @doc[col].nil?||@doc[col][style].nil?
|
27
|
+
title_field, alt_field = col+'_tooltip', col+'_alternative_text'
|
28
|
+
title = @doc[title_field] if model.schema.keys.include?(title_field)
|
29
|
+
alt = @doc[alt_field] if model.schema.keys.include?(alt_field)
|
30
|
+
html_attributes = {:src => "/gridfs/#{@doc[col][style]}", :title => title, :alt => alt}.update(html_attributes)
|
31
|
+
html_attributes = html_attributes.map do |k,v|
|
32
|
+
%{#{k}="#{model.html_escape(v.to_s)}"}
|
33
|
+
end.join(' ')
|
34
|
+
"<img #{html_attributes} />"
|
35
|
+
end
|
36
|
+
|
37
|
+
def fix_type_attachment(k,v)
|
38
|
+
if v=='nil'
|
39
|
+
delete_files_for(k) unless new?
|
40
|
+
@doc[k] = nil
|
41
|
+
elsif v.is_a?(Hash)&&v.key?(:tempfile)
|
42
|
+
delete_files_for(k) unless new?
|
43
|
+
@temp_attachments ||= {}
|
44
|
+
@temp_attachments[k] = v
|
45
|
+
attachment_id = model.gridfs.put(v[:tempfile], {:filename=>v[:filename], :content_type=>v[:type]})
|
46
|
+
@doc[k] = {'original'=>attachment_id}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_files_for(col)
|
51
|
+
obj = (@old_doc||@doc)[col]
|
52
|
+
if obj.respond_to?(:each)
|
53
|
+
obj.each do |k,v|
|
54
|
+
model.gridfs.delete(v)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def after_delete
|
60
|
+
super
|
61
|
+
model.schema.each do |k,v|
|
62
|
+
delete_files_for(k) if v[:type]==:attachment
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def after_save
|
67
|
+
super
|
68
|
+
unless @temp_attachments.nil?
|
69
|
+
@temp_attachments.each do |k,v|
|
70
|
+
after_stash(k)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def after_stash(col); end
|
76
|
+
|
77
|
+
def convert(col, convert_steps, style)
|
78
|
+
return if @doc[col].nil?
|
79
|
+
if @temp_attachments.nil? || @temp_attachments[col].nil?
|
80
|
+
f = model.gridfs.get(@doc[col]['original'])
|
81
|
+
return unless f.content_type[/^image\//]
|
82
|
+
src = Tempfile.new('MongoStash_src')
|
83
|
+
src.binmode
|
84
|
+
src.write(f.read(4096)) until f.eof?
|
85
|
+
src.close
|
86
|
+
@temp_attachments ||= {}
|
87
|
+
@temp_attachments[col] ||= {}
|
88
|
+
@temp_attachments[col][:tempfile] = src
|
89
|
+
@temp_attachments[col][:type] = f.content_type
|
90
|
+
else
|
91
|
+
return unless @temp_attachments[col][:type][/^image\//]
|
92
|
+
src = @temp_attachments[col][:tempfile]
|
93
|
+
end
|
94
|
+
model.gridfs.delete(@doc[col][style]) unless @doc[col][style].nil?
|
95
|
+
ext = style[/[a-zA-Z]+$/].insert(0,'.')
|
96
|
+
content_type = Rack::Mime.mime_type(ext)
|
97
|
+
unless content_type[/^image\//]
|
98
|
+
ext = '.jpg'
|
99
|
+
content_type = 'image/jpeg'
|
100
|
+
end
|
101
|
+
dest = Tempfile.new(['MongoStash_dest', ext])
|
102
|
+
dest.binmode
|
103
|
+
dest.close
|
104
|
+
system "convert \"#{src.path}\" #{convert_steps} \"#{dest.path}\""
|
105
|
+
filename = "#{model.name}/#{self.id}/#{style}"
|
106
|
+
attachment_id = model.gridfs.put(dest.open, {:filename=>filename, :content_type=>content_type})
|
107
|
+
@doc[col] = @doc[col].update({style=>attachment_id})
|
108
|
+
model.collection.update({'_id'=>@doc['_id']}, @doc)
|
109
|
+
#src.close!
|
110
|
+
dest.close!
|
111
|
+
end
|
112
|
+
|
113
|
+
class << self
|
114
|
+
attr_accessor :classes
|
115
|
+
MongoStash.classes = []
|
116
|
+
|
117
|
+
def all_after_stash
|
118
|
+
MongoStash.classes.each do |m|
|
119
|
+
m.all_after_stash
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def fix_dots_in_keys(c, for_real=false)
|
124
|
+
puts "\n#{c}" unless for_real
|
125
|
+
selection = c.schema.select{|k,v| v[:type]==:attachment }
|
126
|
+
img_keys = selection.respond_to?(:keys) ? selection.keys : selection.map{|a|a[0]}
|
127
|
+
c.find({}, {:fields=>img_keys}).each do |e|
|
128
|
+
old_hash = e.doc.select{|k,v| img_keys.include?(k) }
|
129
|
+
fixed_hash = Marshal.load(Marshal.dump(old_hash))
|
130
|
+
img_keys.each do |k|
|
131
|
+
(fixed_hash[k]||{}).keys.each do |style|
|
132
|
+
fixed_hash[k][style.tr('.','_')] = fixed_hash[k].delete(style)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
next if old_hash==fixed_hash
|
136
|
+
|
137
|
+
if for_real
|
138
|
+
c.collection.update({'_id'=>e.id}, {'$set'=>fixed_hash})
|
139
|
+
else
|
140
|
+
puts old_hash.inspect
|
141
|
+
puts fixed_hash.inspect
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def all_fix_dots_in_keys(for_real=false)
|
148
|
+
MongoStash.classes.each do |c|
|
149
|
+
MongoStash.fix_dots_in_keys(c, for_real)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
Encoding.default_internal = Encoding.default_external = Encoding::UTF_8 if RUBY_VERSION >= '1.9.0'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bacon'
|
5
|
+
$:.unshift './lib'
|
6
|
+
require 'mongo_mutation'
|
7
|
+
|
8
|
+
MONGO = ::Mongo::MongoClient.new
|
9
|
+
class NoDB; include MongoMutation; end
|
10
|
+
DB = MONGO['test-mongo-mutation']
|
11
|
+
|
12
|
+
class Naked; include MongoMutation; end
|
13
|
+
|
14
|
+
class Person
|
15
|
+
include MongoMutation
|
16
|
+
slot "name" # no options
|
17
|
+
slot "surname"
|
18
|
+
slot "age", :type=>:integer, :default=>18
|
19
|
+
image_slot "portrait"
|
20
|
+
slot 'subscribed_on', :type=>:date, :default=>proc{Date.today}
|
21
|
+
end
|
22
|
+
|
23
|
+
class Address
|
24
|
+
include MongoMutation
|
25
|
+
slot "body"
|
26
|
+
end
|
27
|
+
|
28
|
+
class Article
|
29
|
+
include MongoMutation
|
30
|
+
slot 'title'
|
31
|
+
slot 'content', :type=>:text
|
32
|
+
image_slot
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "MongoMutation" do
|
36
|
+
|
37
|
+
describe ".db" do
|
38
|
+
it "Should be set to DB constant by default" do
|
39
|
+
NoDB.db.should==nil
|
40
|
+
Naked.db.should==DB
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
shared "Empty BSON" do
|
45
|
+
it "Should be set to an empty BSON ordered hash by default" do
|
46
|
+
@bson.class.should==::BSON::OrderedHash
|
47
|
+
@bson.empty?.should==true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".schema" do
|
52
|
+
before { @bson = Naked.schema }
|
53
|
+
behaves_like "Empty BSON"
|
54
|
+
end
|
55
|
+
|
56
|
+
describe ".relationships" do
|
57
|
+
before { @bson = Naked.relationships }
|
58
|
+
behaves_like "Empty BSON"
|
59
|
+
end
|
60
|
+
|
61
|
+
shared "Basic slot" do
|
62
|
+
it "Adds the declared slot to the schema" do
|
63
|
+
@klass.schema.key?(@key).should==true
|
64
|
+
end
|
65
|
+
it "Defines getters and setters" do
|
66
|
+
@klass.new.respond_to?(@key).should==true
|
67
|
+
@klass.new.respond_to?(@key+'=').should==true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe ".slot" do
|
72
|
+
before { @klass = Person; @key = 'age' }
|
73
|
+
behaves_like "Basic slot"
|
74
|
+
it "Keeps the options in schema" do
|
75
|
+
Person.schema['age'][:default].should==18
|
76
|
+
Person.schema['age'][:type].should==:integer
|
77
|
+
end
|
78
|
+
it "Sets :type option to :string by default if not provided" do
|
79
|
+
Person.schema['name'][:type].should==:string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
shared "Correctly typed" do
|
84
|
+
it "Has the correct type" do
|
85
|
+
@klass.schema[@key][:type].should==@type
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
shared "Image slot" do
|
90
|
+
describe "Reference slot" do
|
91
|
+
before { @type = :attachment }
|
92
|
+
behaves_like "Basic slot"
|
93
|
+
behaves_like "Correctly typed"
|
94
|
+
end
|
95
|
+
describe "Tooltip slot" do
|
96
|
+
before { @key = @key+'_tooltip'; @type = :string }
|
97
|
+
behaves_like "Basic slot"
|
98
|
+
behaves_like "Correctly typed"
|
99
|
+
end
|
100
|
+
describe "Alt text slot" do
|
101
|
+
before { @key = @key+'_alternative_text'; @type = :string }
|
102
|
+
behaves_like "Basic slot"
|
103
|
+
behaves_like "Correctly typed"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe ".image_slot" do
|
108
|
+
describe "Name provided" do
|
109
|
+
before { @klass = Person; @key = 'portrait' }
|
110
|
+
behaves_like "Image slot"
|
111
|
+
end
|
112
|
+
describe "Name not provided" do
|
113
|
+
before { @klass = Article; @key = 'image' }
|
114
|
+
behaves_like "Image slot"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
shared "Has error recipient" do
|
119
|
+
it "Has an empty recipent for errors" do
|
120
|
+
@inst.errors.should=={}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#initialize" do
|
125
|
+
describe "With fields" do
|
126
|
+
before { @inst = Person.new({'age'=>42, 'name'=>'Bozo', 'car'=>'Jaguar'}) }
|
127
|
+
it "Should be flagged as new if the doc has no _id yet" do
|
128
|
+
@inst.new?.should==true
|
129
|
+
end
|
130
|
+
it "Should not be flagged as new if the doc has an _id" do
|
131
|
+
@inst['_id'] = 'abc'
|
132
|
+
@inst.new?.should==false
|
133
|
+
end
|
134
|
+
it "Should pass all keys to the doc" do
|
135
|
+
@inst['name'].should=='Bozo'
|
136
|
+
@inst['car'].should=='Jaguar'
|
137
|
+
end
|
138
|
+
behaves_like "Has error recipient"
|
139
|
+
end
|
140
|
+
describe "Fresh new one" do
|
141
|
+
before { @inst = Person.new }
|
142
|
+
it "Should be flagged as new" do
|
143
|
+
@inst.new?.should==true
|
144
|
+
end
|
145
|
+
it "Has default values correctly set" do
|
146
|
+
@inst['age'].should==18 # direct value
|
147
|
+
@inst['subscribed_on'].class.should==Date # with proc
|
148
|
+
end
|
149
|
+
behaves_like "Has error recipient"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "#model" do
|
154
|
+
it "Is a shortcut for self.class" do
|
155
|
+
Naked.new.model.should==Naked
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
MONGO.drop_database('test-mongo-mutation')
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: campbellhay-mongo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.14
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mickael Riga
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mongo
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.8.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.8.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bacon
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: The CampbellHay Mongo toolbox
|
47
|
+
email: mig@campbellhay.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- .gitignore
|
53
|
+
- Gemfile
|
54
|
+
- campbellhay-mongo.gemspec
|
55
|
+
- gempush
|
56
|
+
- lib/mongo_crushyform.rb
|
57
|
+
- lib/mongo_mutation.rb
|
58
|
+
- lib/mongo_stash.rb
|
59
|
+
- test/spec_mutation.rb
|
60
|
+
homepage: http://www.campbellhay.com
|
61
|
+
licenses: []
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- ./lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.8.23
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: The CampbellHay Mongo toolbox
|
84
|
+
test_files: []
|