mongoid_ext 0.6.1

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.
Files changed (53) hide show
  1. data/.document +5 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +50 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +44 -0
  7. data/VERSION +1 -0
  8. data/bin/mongoid_console +85 -0
  9. data/lib/mongoid_ext.rb +71 -0
  10. data/lib/mongoid_ext/criteria_ext.rb +15 -0
  11. data/lib/mongoid_ext/document_ext.rb +29 -0
  12. data/lib/mongoid_ext/file.rb +86 -0
  13. data/lib/mongoid_ext/file_list.rb +74 -0
  14. data/lib/mongoid_ext/file_server.rb +69 -0
  15. data/lib/mongoid_ext/filter.rb +266 -0
  16. data/lib/mongoid_ext/filter/parser.rb +71 -0
  17. data/lib/mongoid_ext/filter/result_set.rb +75 -0
  18. data/lib/mongoid_ext/js/filter.js +41 -0
  19. data/lib/mongoid_ext/js/find_tags.js +26 -0
  20. data/lib/mongoid_ext/js/tag_cloud.js +28 -0
  21. data/lib/mongoid_ext/modifiers.rb +93 -0
  22. data/lib/mongoid_ext/mongo_mapper.rb +63 -0
  23. data/lib/mongoid_ext/paranoia.rb +100 -0
  24. data/lib/mongoid_ext/patches.rb +17 -0
  25. data/lib/mongoid_ext/random.rb +23 -0
  26. data/lib/mongoid_ext/slugizer.rb +84 -0
  27. data/lib/mongoid_ext/storage.rb +110 -0
  28. data/lib/mongoid_ext/tags.rb +26 -0
  29. data/lib/mongoid_ext/types/embedded_hash.rb +25 -0
  30. data/lib/mongoid_ext/types/open_struct.rb +15 -0
  31. data/lib/mongoid_ext/types/timestamp.rb +15 -0
  32. data/lib/mongoid_ext/types/translation.rb +51 -0
  33. data/lib/mongoid_ext/update.rb +11 -0
  34. data/lib/mongoid_ext/versioning.rb +189 -0
  35. data/lib/mongoid_ext/voteable.rb +104 -0
  36. data/mongoid_ext.gemspec +129 -0
  37. data/test/helper.rb +30 -0
  38. data/test/models.rb +80 -0
  39. data/test/support/custom_matchers.rb +55 -0
  40. data/test/test_filter.rb +51 -0
  41. data/test/test_modifiers.rb +65 -0
  42. data/test/test_paranoia.rb +40 -0
  43. data/test/test_random.rb +57 -0
  44. data/test/test_slugizer.rb +66 -0
  45. data/test/test_storage.rb +110 -0
  46. data/test/test_tags.rb +47 -0
  47. data/test/test_update.rb +16 -0
  48. data/test/test_versioning.rb +55 -0
  49. data/test/test_voteable.rb +77 -0
  50. data/test/types/test_open_struct.rb +22 -0
  51. data/test/types/test_set.rb +26 -0
  52. data/test/types/test_timestamp.rb +40 -0
  53. metadata +301 -0
@@ -0,0 +1,74 @@
1
+ module MongoidExt
2
+ class FileList < EmbeddedHash
3
+ attr_accessor :parent_document
4
+
5
+ def put(id, io, metadata = {})
6
+ if !parent_document.new_record?
7
+ filename = id
8
+ if io.respond_to?(:original_filename)
9
+ filename = io.original_filename
10
+ elsif io.respond_to?(:path) && io.path
11
+ filename = ::File.basename(io.path)
12
+ elsif io.kind_of?(String)
13
+ io = StringIO.new(io)
14
+ end
15
+
16
+ get(id).put(filename, io, metadata)
17
+ else
18
+ (@_pending_files ||= {})[id] = [io, metadata]
19
+ end
20
+ end
21
+
22
+ def files
23
+ ids = self.keys
24
+ ids.delete("_id")
25
+ ids.map {|v| get(v) }
26
+ end
27
+
28
+ def each_file(&block)
29
+ (self.keys-["_id"]).each do |key|
30
+ file = self.get(key)
31
+ yield key, file
32
+ end
33
+ end
34
+
35
+ def get(id)
36
+ if id.kind_of?(MongoidExt::File)
37
+ self[id.id] = id
38
+ return id
39
+ end
40
+
41
+ id = id.to_s.gsub(".", "_")
42
+ file = self[id]
43
+ if file.nil?
44
+ file = self[id] = MongoidExt::File.new
45
+ elsif file.class == BSON::OrderedHash
46
+ file = self[id] = MongoidExt::File.new(file)
47
+ end
48
+
49
+ file._root_document = parent_document
50
+ file
51
+ end
52
+
53
+ def sync_files
54
+ if @_pending_files
55
+ @_pending_files.each do |filename, data|
56
+ put(filename, data[0], data[1])
57
+ end
58
+ @_pending_files = nil
59
+ end
60
+ end
61
+
62
+ def delete(id)
63
+ file = self.get(id)
64
+ super(id)
65
+ file.delete
66
+ end
67
+
68
+ def destroy_files
69
+ each_file do |id, file|
70
+ get(id).delete
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,69 @@
1
+ require 'time'
2
+ require 'rack/utils'
3
+
4
+ module MongoidExt
5
+ class FileServer
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ if env["PATH_INFO"] =~ /^\/_files\/([^\/?]+)/
12
+ @model = $1.classify.constantize rescue nil
13
+ return forbidden if @model.nil?
14
+
15
+ dup._call(env)
16
+ else
17
+ @app.call(env)
18
+ end
19
+ end
20
+
21
+ def _call(env)
22
+ request = Rack::Request.new(env)
23
+ params = request.GET
24
+
25
+ @file = @model.find_file_from_params(params, request)
26
+ return not_found if @file.nil?
27
+
28
+ if @file.present?
29
+ serving
30
+ else
31
+ not_found
32
+ end
33
+ end
34
+
35
+ def forbidden
36
+ body = "Forbidden\n"
37
+ [403, {"Content-Type" => "text/plain",
38
+ "Content-Length" => body.size.to_s,
39
+ "X-Cascade" => "pass"},
40
+ [body]]
41
+ end
42
+
43
+ def serving
44
+ body = self
45
+ [200, {
46
+ "Last-Modified" => Time.now.httpdate,
47
+ "Content-Type" => @file.content_type,
48
+ "Content-Length" => @file.size.to_s
49
+ }, body]
50
+ end
51
+
52
+ def not_found
53
+ body = "File not found: #{@path_info}\n"
54
+ [404, {"Content-Type" => "text/plain",
55
+ "Content-Length" => body.size.to_s,
56
+ "X-Cascade" => "pass"},
57
+ [body]]
58
+ end
59
+
60
+ def each
61
+ f = @file.get
62
+ while part = f.read(8192)
63
+ yield part
64
+ break if part.empty?
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,266 @@
1
+ module MongoidExt
2
+ module Filter
3
+ def self.included(klass)
4
+ begin
5
+ require 'lingua/stemmer'
6
+ rescue LoadError
7
+ $stderr.puts "install ruby-stemmer `gem install ruby-stemmer` to activate the full text search support"
8
+ end
9
+
10
+ klass.class_eval do
11
+ extend ClassMethods
12
+
13
+ field :_keywords, :type => Set, :default => Set.new
14
+ index :_keywords
15
+
16
+ before_save :_update_keywords
17
+
18
+ attr_accessor :search_score
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def filterable_keys(*keys)
24
+ @filterable_keys ||= Set.new
25
+ @filterable_keys += keys
26
+
27
+ @filterable_keys
28
+ end
29
+
30
+ def language(lang = 'en')
31
+ @language ||= lang
32
+ end
33
+
34
+ def filter_conditions(query, opts = {})
35
+ stemmer = nil
36
+ language = opts.delete(:language) || 'en'
37
+
38
+ if defined?(Lingua::Stemmer)
39
+ stemmer = MongoidExt::Filter.build_stemmer(language)
40
+ end
41
+
42
+ @parser = MongoidExt::Filter::Parser.new(stemmer)
43
+ parsed_query = @parser.parse(query)
44
+
45
+ [parsed_query, _build_filter_query(parsed_query)]
46
+ end
47
+
48
+ def filter(query, opts = {})
49
+ min_score = opts.delete(:min_score) || 0.0
50
+ limit = opts.delete(:per_page) || 25
51
+ page = opts.delete(:page) || 1
52
+ select = opts.delete(:select) || self.fields.keys
53
+
54
+ parsed_query, conds = self.filter_conditions(query, opts)
55
+ query = Mongoid::Criteria.new(self)
56
+ conds = query.where(opts).where(conds).selector
57
+
58
+ results = self.db.nolock_eval("function(collection, q, config) { return filter(collection, q, config); }", self.collection_name, conds, {:words => parsed_query[:words].to_a, :stemmed => parsed_query[:stemmed].to_a, :limit => limit, :min_score => min_score, :select => select })
59
+
60
+ pagination = MongoidExt::Filter::ResultSet.new(results["total_entries"], parsed_query, conds, page, limit)
61
+
62
+ pagination.subject = results['results'].map do |result|
63
+ item = self.new(result['doc'])
64
+ item.search_score = result['score']
65
+
66
+ item
67
+ end
68
+
69
+ pagination
70
+ end
71
+
72
+ def _build_filter_query(parsed_query)
73
+ mongoquery = {}
74
+
75
+ if !parsed_query[:tokens].empty?
76
+ keywords = []
77
+
78
+ parsed_query[:tokens].each do |t|
79
+ keywords << /^#{Regexp.escape(t)}/i if t.size > 2
80
+ end
81
+
82
+ mongoquery[:_keywords] = { :$in => keywords }
83
+ end
84
+
85
+ mongoquery.merge!(map_filter_operators(parsed_query[:quotes], parsed_query[:operators]))
86
+
87
+ mongoquery
88
+ end
89
+
90
+ def map_filter_operators(quotes, ops)
91
+ mongoquery = {}
92
+
93
+ if ops["is"]
94
+ ops["is"].each do |d|
95
+ mongoquery[d] = true
96
+ end
97
+ end
98
+
99
+ if ops["not"]
100
+ ops["not"].each do |d|
101
+ mongoquery[d] = false
102
+ end
103
+ end
104
+
105
+ mongoquery
106
+ end
107
+ end
108
+
109
+ protected
110
+ def _update_keywords
111
+ lang = self.class.language
112
+ if lang.kind_of?(Symbol)
113
+ lang = send(lang)
114
+ elsif lang.kind_of?(Proc)
115
+ lang = lang.call(self)
116
+ end
117
+
118
+ stemmer = nil
119
+ if defined?(Lingua)
120
+ stemmer = MongoidExt::Filter.build_stemmer(lang)
121
+ end
122
+
123
+ stop_words = []
124
+ if self.respond_to?("#{lang}_stop_words")
125
+ stop_words = Set.new(self.send("#{lang}_stop_words"))
126
+ end
127
+
128
+ self._keywords = Set.new
129
+ self.class.filterable_keys.each do |key|
130
+ self._keywords += keywords_for_value(read_attribute(key), stemmer, stop_words)
131
+ end
132
+ end
133
+
134
+ private
135
+ def keywords_for_value(val, stemmer=nil, stop_words = [])
136
+ if val.kind_of?(String)
137
+ words = []
138
+ val.downcase.split.each do |word|
139
+ next if word.length < 3
140
+ next if stop_words.include?(word)
141
+
142
+ deword = word.split(%r{'|"|\/|\\|\?|\+|_|\-|\>|\>})
143
+
144
+ deword.each do |word|
145
+ next if word.empty?
146
+
147
+ stem = word
148
+ if stemmer
149
+ stem = stemmer.stem(word)
150
+ end
151
+ normalize_string = lambda {|str| ActiveSupport::Multibyte::Chars.new(str).normalize(:kd).gsub(/[^\x00-\x7F]/n, '').to_s }
152
+
153
+ normalized = normalize_string.call(word)
154
+
155
+ if word != normalized
156
+ words << normalized
157
+ end
158
+
159
+ words << word
160
+ if stem
161
+ normalized=normalize_string.call(stem)
162
+
163
+ words << normalized if stem != normalized
164
+ words << stem if stem != word
165
+ end
166
+ end
167
+ end
168
+
169
+ words
170
+ elsif val.kind_of?(Array)
171
+ val.map { |e| keywords_for_value(e, stemmer, stop_words) }.flatten
172
+ elsif val.kind_of?(Hash)
173
+ val.map { |k, v| keywords_for_value(v, stemmer, stop_words) }.flatten
174
+ elsif val
175
+ [val]
176
+ else
177
+ []
178
+ end
179
+ end
180
+
181
+ def self.build_stemmer(language)
182
+ begin
183
+ return Lingua::Stemmer.new(:language => language)
184
+ rescue Lingua::StemmerError
185
+ return nil
186
+ end
187
+ end
188
+
189
+ def en_stop_words
190
+ ["a", "about", "above", "after", "again", "against", "all", "am", "an",
191
+ "and", "any", "are", "aren't", "as", "at", "be", "because", "been",
192
+ "before", "being", "below", "between", "both", "but", "by", "cannot",
193
+ "can't", "could", "couldn't", "did", "didn't", "do", "does", "doesn't",
194
+ "doing", "don't", "down", "during", "each", "few", "for", "from",
195
+ "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having",
196
+ "he", "he'd", "he'll", "her", "here", "here's", "hers", "herself", "he's",
197
+ "him", "himself", "his", "how", "how's", "i", "i'd", "if", "i'll", "i'm",
198
+ "in", "into", "is", "isn't", "it", "its", "it's", "itself", "i've", "let's",
199
+ "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of",
200
+ "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves",
201
+ "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's",
202
+ "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the",
203
+ "their", "theirs", "them", "themselves", "then", "there", "there's", "these",
204
+ "they", "they'd", "they'll", "they're", "they've", "this", "those", "through",
205
+ "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd",
206
+ "we'll", "were", "we're", "weren't", "we've", "what", "what's", "when",
207
+ "when's", "where", "where's", "which", "while", "who", "whom", "who's",
208
+ "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd",
209
+ "you'll", "your", "you're", "yours", "yourself", "yourselves", "you've",
210
+ "the", "how"]
211
+ end
212
+
213
+ def es_stop_words
214
+ ["de", "la", "que", "el", "en", "y", "a",
215
+ "los", "del", "se", "las", "por", "un", "para", "con", "no", "una", "su",
216
+ "al", "lo", "como", "mas", "pero", "sus", "le", "ya", "o", "este", "sus",
217
+ "si", "porque", "esta", "entre",
218
+ "cuando", "muy", "sin", "sobre", "tambien", "me", "hasta", "hay",
219
+ "donde", "quien", "desde", "todo", "nos", "durante", "todos", "uno", "les",
220
+ "ni", "contra", "otros", "ese", "eso", "ante", "ellos", "e", "esto",
221
+ "mi", "antes", "algunos", "que", "unos", "yo", "otro",
222
+ "otras", "otra", "al", "tanto", "esa", "estos", "mucho", "quienes",
223
+ "nada", "muchos", "cual", "poco", "ella", "estar", "estas", "algunas",
224
+ "algo", "nosotros", "mi", "mis", "tus", "ellas", "sus", "una", "uno",
225
+ "nosotras", "vosostros", "vosostras", "os", "mio", "mia",
226
+ "mios", "mias", "tuyo", "tuya", "tuyos", "tuyas", "suyo",
227
+ "suya", "suyos", "suyas", "nuestro", "nuestra", "nuestros", "nuestras",
228
+ "vuestro", "vuestra", "vuestros", "vuestras", "esos", "esas", "estoy",
229
+ "estas", "esta", "estamos", "estais", "estan",
230
+ "este", "estes", "estemos", "esteis", "esten",
231
+ "estare", "estareis", "estara", "estaremos", "como",
232
+ "estareis", "estarin", "estaria", "estarias",
233
+ "estariamos", "estariais", "estarian", "estaba",
234
+ "estabas", "estabamos", "estabais", "estaban", "estuve",
235
+ "estuviste", "estuvo", "estuvimos", "estuvisteis", "estuvieron", "estuviera",
236
+ "estuvieras", "estuvieramos", "estuvierais", "estuvieran", "estuviese",
237
+ "estuvieses", "estuviesemos", "estuvieseis", "estuviesen", "estando",
238
+ "estado", "estada", "estados", "estadas", "estad", "he", "has", "ha", "hemos",
239
+ "habeis", "han", "haya", "hayas", "hayamos", "hayais", "hayan",
240
+ "habre", "habras", "habra", "habremos", "habreis",
241
+ "habran", "habria", "habrias", "habriamos",
242
+ "habriais", "habrian", "habia", "habias",
243
+ "habiamos", "habiais", "habian", "hube", "hubiste",
244
+ "hubo", "hubimos", "hubisteis", "hubieron", "hubiera", "hubieras", "hubieramos",
245
+ "hubierais", "hubieran", "hubiese", "hubieses", "hubiesemos", "hubieseis",
246
+ "hubiesen", "habiendo", "habido", "habida", "habidos", "habidas", "soy", "eres",
247
+ "es", "somos", "sois", "son", "sea", "seas", "seamos", "seais",
248
+ "sean", "sere", "seras", "sera", "seremos", "sereis",
249
+ "seran", "seria", "serias", "seriamos", "seriais",
250
+ "serian", "era", "eras", "eramos", "erais", "eran", "fui",
251
+ "fuiste", "fue", "fuimos", "fuisteis", "fueron", "fuera", "fueras",
252
+ "fueramos", "fuerais", "fueran", "fuese", "fueses", "fuesemos",
253
+ "fueseis", "fuesen", "sintiendo", "sentido", "sentida", "sentidos", "sentidas",
254
+ "siente", "sentid", "tengo", "tienes", "tiene", "tenemos", "teneis",
255
+ "tienen", "tenga", "tengas", "tengamos", "tengais", "tengan", "tendre",
256
+ "tendras", "tendra", "tendremos", "tendreis", "tendran",
257
+ "tendria", "tendrias", "tendriamos", "tendriais",
258
+ "tendrian", "tenia", "tenias", "teniamos",
259
+ "teniais", "tenian", "tuve", "tuviste", "tuvo", "tuvimos",
260
+ "tuvisteis", "tuvieron", "tuviera", "tuvieras", "tuvieramos",
261
+ "tuvierais", "tuvieran", "tuviese", "tuvieses", "tuviesemos",
262
+ "tuvieseis", "tuviesen", "teniendo", "tenido", "tenida", "tenidos", "tenidas", "tened"]
263
+ end
264
+ end
265
+ end
266
+
@@ -0,0 +1,71 @@
1
+ module MongoidExt
2
+ module Filter
3
+ class Parser
4
+ def initialize(stemmer)
5
+ @stemmer = stemmer
6
+ end
7
+
8
+ def parse(query)
9
+ query, quotes = parse_quoted_text(query)
10
+ query, ops = parse_operators(query)
11
+ query.gsub!(/\s+/, ' ')
12
+ query.gsub!(/\?|\!|\:|\)|\(/, "")
13
+
14
+ words = Set.new(query.downcase.split(/\s+/))
15
+
16
+ stemmed = stem(words)
17
+ tokens = words + stemmed
18
+
19
+ {:query => query,
20
+ :words => words,
21
+ :stemmed => stemmed,
22
+ :operators => ops,
23
+ :tokens => tokens,
24
+ :quotes => quotes}
25
+ end
26
+
27
+ private
28
+ def parse_quoted_text(query)
29
+ quotes = []
30
+ loop do
31
+ m = query.match(/"([^"]+"|-)/)
32
+ if m
33
+ query = $` + $'
34
+ quotes << m[1].gsub('"', "")
35
+ else
36
+ break
37
+ end
38
+ end
39
+
40
+ [query, quotes]
41
+ end
42
+
43
+ def parse_operators(query)
44
+ ops = {}
45
+
46
+ loop do
47
+ m = query.match(/(is|lang|not|by|score):(\S+)/)
48
+ if m
49
+ query = $` + $'
50
+ (ops[m[1]] ||= []) << m[2]
51
+ else
52
+ break
53
+ end
54
+ end
55
+ [query, ops]
56
+ end
57
+
58
+ def stem(words)
59
+ tokens = []
60
+ if @stemmer
61
+ words.each do |word|
62
+ stem = @stemmer.stem(word)
63
+ tokens << stem if word.size > 2 && !words.include?(stem)
64
+ end
65
+ end
66
+
67
+ tokens
68
+ end
69
+ end
70
+ end
71
+ end