my_annotations 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitignore +6 -0
  2. data/.rvmrc +1 -0
  3. data/AUTHORS.rdoc +5 -0
  4. data/CHANGELOG.rdoc +64 -0
  5. data/INDEX.rdoc +17 -0
  6. data/generators/annotations_migration/annotations_migration_generator.rb +32 -0
  7. data/generators/annotations_migration/templates/migration_v1.rb +60 -0
  8. data/generators/annotations_migration/templates/migration_v2.rb +9 -0
  9. data/generators/annotations_migration/templates/migration_v3.rb +74 -0
  10. data/generators/annotations_migration/templates/migration_v4.rb +13 -0
  11. data/install.rb +1 -0
  12. data/lib/annotations/acts_as_annotatable.rb +271 -0
  13. data/lib/annotations/acts_as_annotation_source.rb +117 -0
  14. data/lib/annotations/acts_as_annotation_value.rb +115 -0
  15. data/lib/annotations/config.rb +148 -0
  16. data/lib/annotations/routing.rb +8 -0
  17. data/lib/annotations/util.rb +82 -0
  18. data/lib/annotations_version_fu.rb +119 -0
  19. data/lib/app/controllers/annotations_controller.rb +162 -0
  20. data/lib/app/controllers/application_controller.rb +2 -0
  21. data/lib/app/helpers/application_helper.rb +2 -0
  22. data/lib/app/models/annotation.rb +413 -0
  23. data/lib/app/models/annotation_attribute.rb +37 -0
  24. data/lib/app/models/annotation_value_seed.rb +48 -0
  25. data/lib/app/models/number_value.rb +23 -0
  26. data/lib/app/models/text_value.rb +23 -0
  27. data/my_annotations.gemspec +4 -9
  28. data/rails/init.rb +8 -0
  29. data/test/acts_as_annotatable_test.rb +186 -0
  30. data/test/acts_as_annotation_source_test.rb +84 -0
  31. data/test/acts_as_annotation_value_test.rb +17 -0
  32. data/test/annotation_attribute_test.rb +22 -0
  33. data/test/annotation_test.rb +213 -0
  34. data/test/annotation_value_seed_test.rb +14 -0
  35. data/test/annotation_version_test.rb +39 -0
  36. data/test/annotations_controller_test.rb +27 -0
  37. data/test/app_root/app/controllers/application_controller.rb +9 -0
  38. data/test/app_root/app/models/book.rb +5 -0
  39. data/test/app_root/app/models/chapter.rb +5 -0
  40. data/test/app_root/app/models/group.rb +3 -0
  41. data/test/app_root/app/models/tag.rb +6 -0
  42. data/test/app_root/app/models/user.rb +3 -0
  43. data/test/app_root/app/views/annotations/edit.html.erb +12 -0
  44. data/test/app_root/app/views/annotations/index.html.erb +1 -0
  45. data/test/app_root/app/views/annotations/new.html.erb +11 -0
  46. data/test/app_root/app/views/annotations/show.html.erb +3 -0
  47. data/test/app_root/config/boot.rb +115 -0
  48. data/test/app_root/config/environment.rb +16 -0
  49. data/test/app_root/config/environments/mysql.rb +0 -0
  50. data/test/app_root/config/routes.rb +4 -0
  51. data/test/app_root/db/migrate/001_create_test_models.rb +38 -0
  52. data/test/app_root/db/migrate/002_annotations_migration_v1.rb +60 -0
  53. data/test/app_root/db/migrate/003_annotations_migration_v2.rb +9 -0
  54. data/test/app_root/db/migrate/004_annotations_migration_v3.rb +72 -0
  55. data/test/config_test.rb +383 -0
  56. data/test/fixtures/annotation_attributes.yml +49 -0
  57. data/test/fixtures/annotation_value_seeds.csv +16 -0
  58. data/test/fixtures/annotation_versions.yml +259 -0
  59. data/test/fixtures/annotations.yml +239 -0
  60. data/test/fixtures/books.yml +13 -0
  61. data/test/fixtures/chapters.yml +27 -0
  62. data/test/fixtures/groups.yml +7 -0
  63. data/test/fixtures/number_value_versions.csv +2 -0
  64. data/test/fixtures/number_values.csv +2 -0
  65. data/test/fixtures/text_value_versions.csv +35 -0
  66. data/test/fixtures/text_values.csv +35 -0
  67. data/test/fixtures/users.yml +8 -0
  68. data/test/number_value_version_test.rb +40 -0
  69. data/test/routing_test.rb +27 -0
  70. data/test/test_helper.rb +41 -0
  71. data/test/text_value_version_test.rb +40 -0
  72. metadata +77 -7
@@ -0,0 +1,413 @@
1
+ class Annotation < ActiveRecord::Base
2
+ include AnnotationsVersionFu
3
+
4
+ belongs_to :annotatable,
5
+ :polymorphic => true
6
+
7
+ belongs_to :source,
8
+ :polymorphic => true
9
+
10
+ belongs_to :value,
11
+ :polymorphic => true,
12
+ :autosave => true
13
+
14
+ belongs_to :attribute,
15
+ :class_name => "AnnotationAttribute",
16
+ :foreign_key => "attribute_id"
17
+
18
+ belongs_to :version_creator,
19
+ :class_name => Annotations::Config.user_model_name
20
+
21
+ before_validation :process_value_generation
22
+
23
+ validates_presence_of :source_type,
24
+ :source_id,
25
+ :annotatable_type,
26
+ :annotatable_id,
27
+ :attribute_id,
28
+ :value_type
29
+
30
+ validate :check_annotatable,
31
+ :check_source,
32
+ :check_value,
33
+ :check_duplicate,
34
+ :check_limit_per_source,
35
+ :check_content_restrictions
36
+
37
+
38
+ # ========================
39
+ # Versioning configuration
40
+ # ------------------------
41
+
42
+ annotations_version_fu do
43
+ belongs_to :annotatable,
44
+ :polymorphic => true
45
+
46
+ belongs_to :source,
47
+ :polymorphic => true
48
+
49
+ belongs_to :value,
50
+ :polymorphic => true
51
+
52
+ belongs_to :attribute,
53
+ :class_name => "AnnotationAttribute",
54
+ :foreign_key => "attribute_id"
55
+
56
+ belongs_to :version_creator,
57
+ :class_name => "::#{Annotations::Config.user_model_name}"
58
+
59
+ validates_presence_of :source_type,
60
+ :source_id,
61
+ :annotatable_type,
62
+ :annotatable_id,
63
+ :attribute_id,
64
+ :value_type
65
+
66
+ # NOTE: make sure to update the logic in here
67
+ # if Annotation#value_content changes!
68
+ def value_content
69
+ self.value.nil? ? "" : self.value.ann_content
70
+ end
71
+
72
+ end
73
+
74
+ # ========================
75
+
76
+ # Named scope to allow you to include the value records too.
77
+ # Use this to *potentially* improve performance.
78
+ named_scope :include_values, lambda {
79
+ { :include => [ :value ] }
80
+ }
81
+
82
+ # Finder to get all annotations by a given source.
83
+ named_scope :by_source, lambda { |source_type, source_id|
84
+ { :conditions => { :source_type => source_type,
85
+ :source_id => source_id },
86
+ :order => "created_at DESC" }
87
+ }
88
+
89
+ # Finder to get all annotations for a given annotatable.
90
+ named_scope :for_annotatable, lambda { |annotatable_type, annotatable_id|
91
+ { :conditions => { :annotatable_type => annotatable_type,
92
+ :annotatable_id => annotatable_id },
93
+ :order => "created_at DESC" }
94
+ }
95
+
96
+ # Finder to get all annotations with a given attribute_name.
97
+ named_scope :with_attribute_name, lambda { |attrib_name|
98
+ { :conditions => { :annotation_attributes => { :name => attrib_name } },
99
+ :joins => :attribute,
100
+ :order => "created_at DESC" }
101
+ }
102
+
103
+ # Finder to get all annotations with one of the given attribute_names.
104
+ named_scope :with_attribute_names, lambda { |attrib_names|
105
+ conditions = [attrib_names.collect{"annotation_attributes.name = ?"}.join(" or ")] | attrib_names
106
+ { :conditions => conditions,
107
+ :joins => :attribute,
108
+ :order => "created_at DESC" }
109
+ }
110
+
111
+ # Finder to get all annotations for a given value_type.
112
+ named_scope :with_value_type, lambda { |value_type|
113
+ { :conditions => { :value_type => value_type },
114
+ :order => "created_at DESC" }
115
+ }
116
+
117
+ # Helper class method to look up an annotatable object
118
+ # given the annotatable class name and ID.
119
+ def self.find_annotatable(annotatable_type, annotatable_id)
120
+ return nil if annotatable_type.nil? or annotatable_id.nil?
121
+ begin
122
+ return annotatable_type.constantize.find(annotatable_id)
123
+ rescue
124
+ return nil
125
+ end
126
+ end
127
+
128
+ # Helper class method to look up a source object
129
+ # given the source class name and ID.
130
+ def self.find_source(source_type, source_id)
131
+ return nil if source_type.nil? or source_id.nil?
132
+ begin
133
+ return source_type.constantize.find(source_id)
134
+ rescue
135
+ return nil
136
+ end
137
+ end
138
+
139
+ def attribute_name
140
+ self.attribute.name
141
+ end
142
+
143
+ def attribute_name=(attr_name)
144
+ attr_name = attr_name.to_s.strip
145
+ self.attribute = AnnotationAttribute.find_or_create_by_name(attr_name)
146
+ end
147
+
148
+ alias_method :original_set_value=, :value=
149
+ def value=(value_in)
150
+ # Store this raw value in a temporary variable for
151
+ # later processing before the object is saved.
152
+ @raw_value = value_in
153
+ end
154
+
155
+ def value_content
156
+ self.value.nil? ? "" : self.value.ann_content
157
+ end
158
+
159
+ def self.create_multiple(params, separator)
160
+ success = true
161
+ annotations = [ ]
162
+ errors = [ ]
163
+
164
+ annotatable = Annotation.find_annotatable(params[:annotatable_type], params[:annotatable_id])
165
+
166
+ if annotatable
167
+ values = params[:value]
168
+
169
+ # Remove value from params hash
170
+ params.delete("value")
171
+
172
+ values.split(separator).each do |val|
173
+ ann = Annotation.new(params)
174
+ ann.value = val.strip
175
+
176
+ if ann.save
177
+ annotations << ann
178
+ else
179
+ error_text = "Error(s) occurred whilst saving annotation with attribute: '#{params[:attribute_name]}', and value: #{val} - #{ann.errors.full_messages.to_sentence}."
180
+ errors << error_text
181
+ logger.info(error_text)
182
+ success = false
183
+ end
184
+ end
185
+ else
186
+ errors << "Annotatable object doesn't exist"
187
+ success = false
188
+ end
189
+
190
+ return [ success, annotations, errors ]
191
+ end
192
+
193
+ protected
194
+
195
+ def ok_value_object_type?
196
+ return !self.value.nil? &&
197
+ self.value.is_a?(ActiveRecord::Base) &&
198
+ self.value.class.respond_to?(:is_annotation_value)
199
+ end
200
+
201
+ def process_value_generation
202
+ if defined?(@raw_value) && !@raw_value.blank?
203
+ val = process_value_adjustments(@raw_value)
204
+ val = try_use_value_factory(val)
205
+
206
+ # Now run default value generation logic
207
+ # (as a fallback for default cases)
208
+ case val
209
+ when String, Symbol
210
+ val = TextValue.new :text => val.to_s
211
+ when Numeric
212
+ val = NumberValue.new :number => val
213
+ when ActiveRecord::Base
214
+ # Do nothing
215
+ else
216
+ self.errors.add(:value, "is not a valid value object")
217
+ end
218
+
219
+ # Set it on the ActiveRecord level now
220
+ self.original_set_value = val
221
+
222
+ # Reset the internal raw value too, in case this is rerun
223
+ @raw_value = val
224
+ end
225
+
226
+ return true
227
+ end
228
+
229
+ def process_value_adjustments(value_in)
230
+ value_out = value_in
231
+
232
+ attr_name = self.attribute_name.downcase
233
+
234
+ value_in = value_out.to_s if value_out.is_a?(Symbol)
235
+
236
+ # Make lowercase or uppercase if required
237
+ if value_out.is_a?(String)
238
+ if Annotations::Config::attribute_names_for_values_to_be_downcased.include?(attr_name)
239
+ value_out = value_out.downcase
240
+ end
241
+ if Annotations::Config::attribute_names_for_values_to_be_upcased.include?(attr_name)
242
+ value_out = value_out.upcase
243
+ end
244
+
245
+ # Apply strip text rules
246
+ Annotations::Config::strip_text_rules.each do |attr, strip_rules|
247
+ if attr_name == attr.downcase
248
+ if strip_rules.is_a? Array
249
+ strip_rules.each do |s|
250
+ value_out = value_out.gsub(s, '')
251
+ end
252
+ elsif strip_rules.is_a? String or strip_rules.is_a? Regexp
253
+ value_out = value_out.gsub(strip_rules, '')
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ return value_out
260
+ end
261
+
262
+ def try_use_value_factory(value_in)
263
+ attr_name = self.attribute_name.downcase
264
+
265
+ if Annotations::Config::value_factories.has_key?(attr_name)
266
+ return Annotations::Config::value_factories[attr_name].call(value_in)
267
+ else
268
+ return value_in
269
+ end
270
+ end
271
+
272
+ # ===========
273
+ # Validations
274
+ # -----------
275
+
276
+ def check_annotatable
277
+ if Annotation.find_annotatable(self.annotatable_type, self.annotatable_id).nil?
278
+ self.errors.add(:annotatable_id, "doesn't exist")
279
+ return false
280
+ else
281
+ return true
282
+ end
283
+ end
284
+
285
+ def check_source
286
+ if Annotation.find_source(self.source_type, self.source_id).nil?
287
+ self.errors.add(:source_id, "doesn't exist")
288
+ return false
289
+ else
290
+ return true
291
+ end
292
+ end
293
+
294
+ def check_value
295
+ ok = true
296
+ if self.value.nil?
297
+ self.errors.add(:value, "object must be provided")
298
+ ok = false
299
+ elsif !ok_value_object_type?
300
+ self.errors.add(:value, "must be a valid annotation value object")
301
+ ok = false
302
+ else
303
+ attr_name = self.attribute_name.downcase
304
+ if Annotations::Config::valid_value_types.has_key?(attr_name) &&
305
+ !([ Annotations::Config::valid_value_types[attr_name] ].flatten.include?(self.value.class.name))
306
+ self.errors.add_to_base("Annotation value is of an invalid type for attribute name: '#{attr_name}'. Provided value is a #{self.value.class.name}.")
307
+ ok = false
308
+ end
309
+ end
310
+
311
+ return ok
312
+ end
313
+
314
+ # This method checks whether duplicates are allowed for this particular annotation type (ie:
315
+ # for the attribute that this annotation belongs to).
316
+ # If not allowed, it checks for a duplicate existing annotation and fails if one does exist.
317
+ def check_duplicate
318
+ if ok_value_object_type?
319
+ attr_name = self.attribute_name.downcase
320
+ if Annotations::Config.attribute_names_to_allow_duplicates.include?(attr_name)
321
+ return true
322
+ else
323
+ if self.value.class.has_duplicate_annotation?(self)
324
+ self.errors.add_to_base("This annotation already exists and is not allowed to be created again.")
325
+ return false
326
+ else
327
+ return true
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ # This method uses the 'limits_per_source config' setting to check whether a limit has been reached.
334
+ #
335
+ # NOTE: this check is only carried out on new records, not records that are being updated.
336
+ def check_limit_per_source
337
+ attr_name = self.attribute_name.downcase
338
+ if self.new_record? and Annotations::Config::limits_per_source.has_key?(attr_name)
339
+ options = Annotations::Config::limits_per_source[attr_name]
340
+ max = options[0]
341
+ can_replace = options[1]
342
+
343
+ unless (found_annotatable = Annotation.find_annotatable(self.annotatable_type, self.annotatable_id)).nil?
344
+ anns = found_annotatable.annotations_with_attribute_and_by_source(attr_name, self.source)
345
+
346
+ if anns.length >= max
347
+ self.errors.add_to_base("The limit has been reached for annotations with this attribute and by this source.")
348
+ return false
349
+ else
350
+ return true
351
+ end
352
+ else
353
+ return true
354
+ end
355
+ else
356
+ return true
357
+ end
358
+ end
359
+
360
+ def check_content_restrictions
361
+ if ok_value_object_type?
362
+ attr_name = self.attribute_name.downcase
363
+ content_to_check = self.value_content
364
+ if Annotations::Config::content_restrictions.has_key?(attr_name)
365
+ options = Annotations::Config::content_restrictions[attr_name]
366
+
367
+ case options[:in]
368
+ when Array
369
+ if content_to_check.is_a?(String)
370
+ if options[:in].map{|s| s.downcase}.include?(content_to_check.downcase)
371
+ return true
372
+ else
373
+ self.errors.add_to_base(options[:error_message])
374
+ return false
375
+ end
376
+ else
377
+ if options[:in].include?(content_to_check)
378
+ return true
379
+ else
380
+ self.errors.add_to_base(options[:error_message])
381
+ return false
382
+ end
383
+ end
384
+
385
+ when Range
386
+ # Need to take into account that "a_string".to_i == 0
387
+ if content_to_check == "0"
388
+ if options[:in] === 0
389
+ return true
390
+ else
391
+ self.errors.add_to_base(options[:error_message])
392
+ return false
393
+ end
394
+ else
395
+ if options[:in] === content_to_check.to_i
396
+ return true
397
+ else
398
+ self.errors.add_to_base(options[:error_message])
399
+ return false
400
+ end
401
+ end
402
+
403
+ else
404
+ return true
405
+ end
406
+ else
407
+ return true
408
+ end
409
+ end
410
+ end
411
+
412
+ # ===========
413
+ end
@@ -0,0 +1,37 @@
1
+ class AnnotationAttribute < ActiveRecord::Base
2
+ validates_presence_of :name,
3
+ :identifier
4
+
5
+ validates_uniqueness_of :name,
6
+ :case_sensitive => false
7
+
8
+ validates_uniqueness_of :identifier,
9
+ :case_sensitive => false
10
+
11
+ has_many :annotations,
12
+ :foreign_key => "attribute_id"
13
+
14
+ # If the identifier is not set, generate it before validation takes place.
15
+ # See Annotations::Config::default_attribute_identifier_template
16
+ # for more info.
17
+ #
18
+ # The rules are:
19
+ # - if an identifier is manually set, nothing happens.
20
+ # - if no identifier is set:
21
+ # - if name is enclosed in chevrons (eg: <http://...>) then the chevrons are taken out and the result is the new identifier.
22
+ # - if name is a URI beginning with http:// or urn: then this is used directly as the identifier.
23
+ # - in all other cases the identifier will be generated using the template specified by
24
+ # Annotations::Config::default_attribute_identifier_template, where '%s' in the template will be replaced with
25
+ # the transformation of 'name' through the Proc specified by Annotations::Config::attribute_name_transform_for_identifier.
26
+ def before_validation
27
+ unless self.name.blank? or !self.identifier.blank?
28
+ if self.name.match(/^<.+>$/)
29
+ self.identifier = self.name[1, self.name.length-1].chop
30
+ elsif self.name.match(/^http:\/\//) or self.name.match(/^urn:/)
31
+ self.identifier = self.name
32
+ else
33
+ self.identifier = (Annotations::Config::default_attribute_identifier_template % Annotations::Config::attribute_name_transform_for_identifier.call(self.name))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ class AnnotationValueSeed < ActiveRecord::Base
2
+ validates_presence_of :attribute_id,
3
+ :value_type,
4
+ :value_id
5
+
6
+ belongs_to :value,
7
+ :polymorphic => true
8
+
9
+ belongs_to :attribute,
10
+ :class_name => "AnnotationAttribute",
11
+ :foreign_key => "attribute_id"
12
+
13
+ # Named scope to allow you to include the value records too.
14
+ # Use this to *potentially* improve performance.
15
+ named_scope :include_values, lambda {
16
+ { :include => [ :value ] }
17
+ }
18
+
19
+ # Finder to get all annotation value seeds with a given attrib_name.
20
+ named_scope :with_attribute_name, lambda { |attrib_name|
21
+ { :conditions => { :annotation_attributes => { :name => attrib_name } },
22
+ :joins => :attribute,
23
+ :order => "created_at DESC" }
24
+ }
25
+
26
+ # Finder to get all annotation value seeds with one of the given attrib_names.
27
+ named_scope :with_attribute_names, lambda { |attrib_names|
28
+ conditions = [attrib_names.collect{"annotation_attributes.name = ?"}.join(" or ")] | attrib_names
29
+ { :conditions => conditions,
30
+ :joins => :attribute,
31
+ :order => "created_at DESC" }
32
+ }
33
+
34
+ # Finder to get all annotations for a given value_type.
35
+ named_scope :with_value_type, lambda { |value_type|
36
+ { :conditions => { :value_type => value_type },
37
+ :order => "created_at DESC" }
38
+ }
39
+
40
+ def self.find_by_attribute_name(attr_name)
41
+ return [] if attr_name.blank?
42
+
43
+ AnnotationValueSeed.find(:all,
44
+ :joins => [ :attribute ],
45
+ :conditions => { :annotation_attributes => { :name => attr_name } },
46
+ :order => "created_at DESC")
47
+ end
48
+ end
@@ -0,0 +1,23 @@
1
+ class NumberValue < ActiveRecord::Base
2
+ include AnnotationsVersionFu
3
+
4
+ validates_presence_of :number
5
+
6
+ acts_as_annotation_value :content_field => :number
7
+
8
+ belongs_to :version_creator,
9
+ :class_name => "::#{Annotations::Config.user_model_name}"
10
+
11
+ # ========================
12
+ # Versioning configuration
13
+ # ------------------------
14
+
15
+ annotations_version_fu do
16
+ validates_presence_of :number
17
+
18
+ belongs_to :version_creator,
19
+ :class_name => "::#{Annotations::Config.user_model_name}"
20
+ end
21
+
22
+ # ========================
23
+ end
@@ -0,0 +1,23 @@
1
+ class TextValue < ActiveRecord::Base
2
+ include AnnotationsVersionFu
3
+
4
+ validates_presence_of :text
5
+
6
+ acts_as_annotation_value :content_field => :text
7
+
8
+ belongs_to :version_creator,
9
+ :class_name => "::#{Annotations::Config.user_model_name}"
10
+
11
+ # ========================
12
+ # Versioning configuration
13
+ # ------------------------
14
+
15
+ annotations_version_fu do
16
+ validates_presence_of :text
17
+
18
+ belongs_to :version_creator,
19
+ :class_name => "::#{Annotations::Config.user_model_name}"
20
+ end
21
+
22
+ # ========================
23
+ end
@@ -1,19 +1,14 @@
1
+ require 'rake'
2
+
1
3
  Gem::Specification.new do |s|
2
4
  s.name = 'my_annotations'
3
- s.version = '0.5.0'
5
+ s.version = '0.5.1'
4
6
  s.date = '2013-05-02'
5
7
  s.summary = "This gem allows arbitrary metadata and relationships to be stored and retrieved, in the form of Annotations for any model objects in your Ruby on Rails (v2.2+) application."
6
8
  s.description = "This gem allows arbitrary metadata and relationships to be stored and retrieved, in the form of Annotations for any model objects in your Ruby on Rails (v2.2+) application."
7
9
  s.authors = ["Jiten Bhagat","Stuart Owen","Quyen Nguyen"]
8
10
  s.email = 'nttqa22001@yahoo.com'
9
- s.files = ["lib/my_annotations.rb",
10
- "RakeFile",
11
- "VERSION.yml",
12
- "LICENSE",
13
- "script/console",
14
- "README.rdoc",
15
- "RUNNING_TESTS.rdoc",
16
- "my_annotations.gemspec"]
11
+ s.files = `git ls-files`.split($/)
17
12
  s.homepage = 'https://github.com/myGrid/annotations'
18
13
  s.require_paths = ["lib"]
19
14
  end
data/rails/init.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'my_annotations.rb'
2
+
3
+ # FIX for engines model reloading issue in development mode
4
+ if ENV['RAILS_ENV'] != 'production'
5
+ load_paths.each do |path|
6
+ ActiveSupport::Dependencies.autoload_once_paths.delete(path)
7
+ end
8
+ end