C0 code coverage information
Generated on Tue Oct 16 11:40:49 -0400 2007 with rcov 0.8.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 # Copyright (C) 2004-2006 Laurent Sansonetti
2 #
3 # Alexandria is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License as
5 # published by the Free Software Foundation; either version 2 of the
6 # License, or (at your option) any later version.
7 #
8 # Alexandria is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public
14 # License along with Alexandria; see the file COPYING. If not,
15 # write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
16 # Boston, MA 02111-1307, USA.
17
18 module Alexandria
19 class SmartLibrary < Array
20 include GetText
21 extend GetText
22 bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
23
24 ALL_RULES, ANY_RULE = 1, 2
25 attr_reader :name
26 attr_accessor :rules, :predicate_operator_rule
27
28 DIR = File.join(ENV['HOME'], '.alexandria', '.smart_libraries')
29 EXT = '.yaml'
30
31 def initialize(name, rules, predicate_operator_rule)
32 super()
33 raise if name.nil? or rules.nil? or predicate_operator_rule.nil?
34 @name = name
35 @rules = rules
36 @predicate_operator_rule = predicate_operator_rule
37 save
38 libraries = Libraries.instance
39 libraries.add_observer(self)
40 self.libraries = libraries.all_regular_libraries
41 @cache = {}
42 end
43
44 def self.loadall
45 a = []
46 begin
47 # Deserialize smart libraries.
48 Dir.chdir(DIR) do
49 Dir["*" + EXT].each do |filename|
50 # Skip non-regular files.
51 next unless File.stat(filename).file?
52
53 text = IO.read(filename)
54 hash = YAML.load(text)
55 begin
56 smart_library = self.from_hash(hash)
57 a << smart_library
58 rescue => e
59 puts "Cannot load serialized smart library: #{e}"
60 puts e.backtrace
61 end
62 end
63 end
64
65 rescue Errno::ENOENT
66 # First run and no smart libraries yet? Provide some default
67 # ones.
68 self.sample_smart_libraries.each do |smart_library|
69 smart_library.save
70 a << smart_library
71 end
72 end
73 a.each { |x| x.refilter }
74 return a
75 end
76
77 def self.sample_smart_libraries
78 a = []
79
80 operands = Rule::Operands::LEFT
81
82 # Favorite books.
83 rule = Rule.new(operands.find { |x| x.book_selector == :rating },
84 Rule::Operators::IS,
85 UI::MainApp::MAX_RATING_STARS.to_s)
86 a << self.new(_("Favorite"), [rule], ALL_RULES)
87
88 # Loaned books.
89 rule = Rule.new(operands.find { |x| x.book_selector == :loaned },
90 Rule::Operators::IS_TRUE,
91 nil)
92 a << self.new(_("Loaned"), [rule], ALL_RULES)
93
94 #Redd books.
95 rule = Rule.new(operands.find { |x| x.book_selector == :redd },
96 Rule::Operators::IS_TRUE,
97 nil)
98 a << self.new(_("Read"), [rule], ALL_RULES)
99
100 #Own books.
101 rule = Rule.new(operands.find { |x| x.book_selector == :own },
102 Rule::Operators::IS_TRUE,
103 nil)
104 a << self.new(_("Owned"), [rule], ALL_RULES)
105
106 #Want books.
107 rule = Rule.new(operands.find { |x| x.book_selector == :want },
108 Rule::Operators::IS_TRUE,
109 nil)
110 rule2 = Rule.new(operands.find { |x| x.book_selector == :own },
111 Rule::Operators::IS_NOT_TRUE,
112 nil)
113 a << self.new(_("Wishlist"), [rule, rule2], ALL_RULES)
114
115
116 return a
117 end
118
119 def self.from_hash(hash)
120 SmartLibrary.new(hash[:name],
121 hash[:rules].map { |x| Rule.from_hash(x) },
122 hash[:predicate_operator_rule] == :all \
123 ? ALL_RULES : ANY_RULE)
124 end
125
126 def to_hash
127 {
128 :name => @name,
129 :predicate_operator_rule =>
130 @predicate_operator_rule == ALL_RULES ? :all : :any,
131 :rules => @rules.map { |x| x.to_hash }
132 }
133 end
134
135 def name=(new_name)
136 if @name != new_name
137 old_yaml = self.yaml
138 @name = new_name
139 FileUtils.mv(old_yaml, self.yaml)
140 save
141 end
142 end
143
144 def update(*params)
145 if params.first.is_a?(Libraries)
146 libraries, action, library = params
147 unless library.is_a?(self.class)
148 self.libraries = libraries.all_libraries
149 refilter
150 end
151 elsif params.first.is_a?(Library)
152 refilter
153 end
154 end
155
156 def refilter
157 raise "need libraries" if @libraries.nil?
158 raise "no libraries" if @libraries.empty?
159 raise "need predicate operator" if @predicate_operator_rule.nil?
160 raise "need rule" if @rules.nil? or @rules.empty?
161
162 filters = @rules.map { |x| x.filter_proc }
163 selector = @predicate_operator_rule == ALL_RULES ? :all? : :any?
164
165 self.clear
166 @cache.clear
167
168 @libraries.each do |library|
169 filtered_library = library.select do |book|
170 filters.send(selector) { |filter| filter.call(book) } #Problem here.
171 end
172 filtered_library.each { |x| @cache[x] = library }
173 self.concat(filtered_library)
174 end
175 @n_rated = select { |x| !x.rating.nil? and x.rating > 0 }.length
176 end
177
178 def cover(book)
179 @cache[book].cover(book)
180 end
181
182 def yaml(book=nil)
183 if book
184 @cache[book].yaml(book)
185 else
186 File.join(DIR, @name + EXT)
187 end
188 end
189
190 def save(book=nil)
191 if book
192 @cache[book].save(book)
193 else
194 FileUtils.mkdir_p(DIR)
195 File.open(self.yaml, "w") { |io| io.puts self.to_hash.to_yaml }
196 end
197 end
198
199 def save_cover(book, cover_uri)
200 @cache[book].save_cover(book)
201 end
202
203 def cover(book)
204 @cache[book].cover(book)
205 end
206
207 def final_cover(book)
208 @cache[book].final_cover(book)
209 end
210
211 def copy_covers(somewhere)
212 FileUtils.rm_rf(somewhere) if File.exists?(somewhere)
213 FileUtils.mkdir(somewhere)
214 each do |book|
215 library = @cache[book]
216 next unless File.exists?(library.cover(book))
217 FileUtils.cp(File.join(library.path,
218 book.ident + Library::EXT[:cover]),
219 File.join(somewhere,
220 library.final_cover(book)))
221 end
222 end
223
224 def n_rated
225 @n_rated
226 end
227
228 def n_unrated
229 length - n_rated
230 end
231
232 def ==(object)
233 object.is_a?(self.class) && object.name == self.name
234 end
235
236 @@deleted_libraries = []
237
238 def self.deleted_libraries
239 @@deleted_libraries
240 end
241
242 def self.really_delete_deleted_libraries
243 @@deleted_libraries.each do |library|
244 puts "Deleting smart library file (#{self.yaml})" if $DEBUG
245 FileUtils.rm_rf(library.yaml)
246 end
247 end
248
249 def delete
250 raise if @@deleted_libraries.include?(self)
251 @@deleted_libraries << self
252 end
253
254 def deleted?
255 @@deleted_libraries.include?(self)
256 end
257
258 def undelete
259 raise unless @@deleted_libraries.include?(self)
260 @@deleted_libraries.delete(self)
261 end
262
263 #######
264 private
265 #######
266
267 def libraries=(ary)
268 @libraries.each { |x| x.delete_observer(self) } if @libraries
269 @libraries = ary.select { |x| x.is_a?(Library) }
270 @libraries.each { |x| x.add_observer(self) }
271 end
272
273 ######
274 public
275 ######
276
277 class Rule
278 include GetText
279 extend GetText
280 bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
281
282 attr_accessor :operand, :operation, :value
283
284 def initialize(operand, operation, value)
285 raise if operand.nil? or operation.nil? # value can be nil
286 @operand = operand
287 @operation = operation
288 @value = value
289 end
290
291 def self.from_hash(hash)
292 operand = Operands::LEFT.find do |x|
293 x.book_selector == hash[:operand]
294 end
295 operator = Operators::ALL.find do |x|
296 x.sym == hash[:operation]
297 end
298 Rule.new(operand, operator, hash[:value])
299 end
300
301 def to_hash
302 {
303 :operand => @operand.book_selector,
304 :operation => @operation.sym,
305 :value => @value
306 }
307 end
308
309 class Operand < Struct.new(:name, :klass)
310 def <=>(x)
311 self.name <=> x.name
312 end
313 end
314
315 class LeftOperand < Operand
316 attr_accessor :book_selector
317
318 def initialize(book_selector, *args)
319 super(*args)
320 @book_selector = book_selector
321 end
322 end
323
324 class Operator < Struct.new(:sym, :name, :proc)
325 def <=>(x)
326 self.name <=> x.name
327 end
328 end
329
330 module Operands
331 include GetText
332 extend GetText
333 bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
334
335 LEFT = [
336 LeftOperand.new(:title, _("Title"), String),
337 LeftOperand.new(:isbn, _("ISBN"), String),
338 LeftOperand.new(:authors, _("Authors"), String),
339 LeftOperand.new(:publisher, _("Publisher"), String),
340 LeftOperand.new(:publish_year, _("Publish Year"), Integer),
341 LeftOperand.new(:edition, _("Binding"), String),
342 LeftOperand.new(:rating, _("Rating"), Integer),
343 LeftOperand.new(:notes, _("Notes"), String),
344 LeftOperand.new(:loaned, _("Loaning State"), TrueClass),
345 LeftOperand.new(:loaned_since, _("Loaning Date"), Time),
346 LeftOperand.new(:loaned_to, _("Loaning Person"), String),
347 LeftOperand.new(:redd, _("Read"), TrueClass),
348 LeftOperand.new(:own, _("Own"), TrueClass),
349 LeftOperand.new(:want, _("Want"), TrueClass),
350 ].sort
351
352 STRING = Operand.new(nil, String)
353 INTEGER = Operand.new(nil, Integer)
354 TIME = Operand.new(nil, Time)
355 DAYS = Operand.new(_("days"), Integer)
356 end
357
358 module Operators
359 include GetText
360 extend GetText
361 bindtextdomain(Alexandria::TEXTDOMAIN, nil, nil, "UTF-8")
362
363 IS_TRUE = Operator.new(
364 :is_true,
365 _("is set"),
366 proc { |x| x })
367 IS_NOT_TRUE = Operator.new(
368 :is_not_true,
369 _("is not set"),
370 proc { |x| !x })
371 IS = Operator.new(
372 :is,
373 _("is"),
374 proc { |x, y| x == y })
375 IS_NOT = Operator.new(
376 :is_not,
377 _("is not"),
378 proc { |x, y| x != y })
379 CONTAINS = Operator.new(
380 :contains,
381 _("contains"),
382 proc { |x, y| x.include?(y) })
383 DOES_NOT_CONTAIN = Operator.new(
384 :does_not_contain,
385 _("does not contain"),
386 proc { |x, y| !x.include?(y) })
387 STARTS_WITH = Operator.new(
388 :starts_with,
389 _("starts with"),
390 proc { |x, y| /^#{y}/.match(x) })
391 ENDS_WITH = Operator.new(
392 :ends_with,
393 _("ends with"),
394 proc { |x, y| /#{y}$/.match(x) })
395 IS_GREATER_THAN = Operator.new(
396 :is_greater_than,
397 _("is greater than"),
398 proc { |x, y| x > y })
399 IS_LESS_THAN = Operator.new(
400 :is_less_than,
401 _("is less than"),
402 proc { |x, y| x < y })
403 IS_AFTER = Operator.new(
404 :is_after,
405 _("is after"),
406 IS_GREATER_THAN.proc)
407 IS_BEFORE = Operator.new(
408 :is_before,
409 _("is before"),
410 IS_LESS_THAN.proc)
411 IS_IN_LAST = Operator.new(
412 :is_in_last_days,
413 _("is in last"),
414 proc { |x, y| Time.now - x <= 3600*24*y })
415 IS_NOT_IN_LAST = Operator.new(
416 :is_not_in_last_days,
417 _("is not in last"),
418 proc { |x, y| Time.now - x > 3600*24*y })
419
420 ALL = self.constants.map \
421 { |x| self.module_eval(x) }.select \
422 { |x| x.is_a?(Operator) }
423 end
424
425 BOOLEAN_OPERATORS = [
426 Operators::IS_TRUE,
427 Operators::IS_NOT_TRUE
428 ].sort
429
430 STRING_OPERATORS = [
431 Operators::IS,
432 Operators::IS_NOT,
433 Operators::CONTAINS,
434 Operators::DOES_NOT_CONTAIN,
435 Operators::STARTS_WITH,
436 Operators::ENDS_WITH
437 ].sort
438
439 INTEGER_OPERATORS = [
440 Operators::IS,
441 Operators::IS_NOT,
442 Operators::IS_GREATER_THAN,
443 Operators::IS_LESS_THAN
444 ].sort
445
446 TIME_OPERATORS = [
447 Operators::IS,
448 Operators::IS_NOT,
449 Operators::IS_AFTER,
450 Operators::IS_BEFORE,
451 Operators::IS_IN_LAST,
452 Operators::IS_NOT_IN_LAST
453 ].sort
454
455 def self.operations_for_operand(operand)
456 case operand.klass.name
457 when 'String'
458 STRING_OPERATORS.map { |x| [x, Operands::STRING] }
459 when 'Integer'
460 INTEGER_OPERATORS.map { |x| [x, Operands::INTEGER] }
461 when 'TrueClass'
462 BOOLEAN_OPERATORS.map { |x| [x, nil] }
463 when 'Time'
464 TIME_OPERATORS.map do |x|
465 if x == Operators::IS_IN_LAST or
466 x == Operators::IS_NOT_IN_LAST
467
468 [x, Operands::DAYS]
469 else
470 [x, Operands::TIME]
471 end
472 end
473 else
474 raise "invalid operand klass #{operand.klass}"
475 end
476 end
477
478 def filter_proc
479 proc do |book|
480 begin
481 left_value = book.send(@operand.book_selector)
482 rescue => e
483 puts e.message
484 end
485 right_value = @value
486 if right_value.is_a?(String)
487 left_value = left_value.to_s.downcase
488 right_value = right_value.downcase
489 end
490 params = [left_value]
491 params << right_value unless right_value.nil?
492 @operation.proc.call(*params)
493 end
494 end
495 end
496 end
497 end
Generated using the rcov code coverage analysis tool for Ruby version 0.8.0.