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
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ test/app_root/log/
2
+ doc/
3
+ created.rid
4
+ *.*~
5
+ .svn/
6
+ *.gem
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use ree-1.8.7-2010.02@annotations
data/AUTHORS.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = Authors
2
+
3
+ == Original Author
4
+
5
+ Jiten Bhagat (mailto:mail@jits.co.uk), as part of the BioCatalogue project.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ = Changelog
2
+
3
+ == 0.4.1
4
+ * Add +Annotation::include_values+ as a named scope that can be used to potentially improve performance.
5
+ * Added +include_values+ optional argument (default: false) to as many finder methods. This allows you to specify whether value records must be included in the query results or not, and *may* be used to improve performance.
6
+
7
+ == 0.4.0
8
+ * New config option: +Annotations::Config.value_factories+ - support for value object generation factories per attribute name.
9
+ Example:
10
+ Annotations::Config.value_factories["tag"] = Proc.new { |v|
11
+ case v
12
+ when String, Symbol
13
+ Tag.find_or_create_by_name(v.to_s)
14
+ else
15
+ v
16
+ end
17
+ }
18
+ IMPORTANT: don't use explicit +return+s in your Proc otherwise it will cause the returning method to exit too!
19
+ * The process of generating/setting the annotation's actual value object has been changed to "lazy generate" the actual +value+ object of the annotation. This is now done before validation. NOTE: this still allows +Annotation#value+ (which has been overridden) to be set at any time, but the actual setting of the value association at the ActiveRecord level now happens later.
20
+ * The +process_value_adjustments+ code in the +Annotation+ model now happens BEFORE setting the value association. This prevents the value object from being modified after it's been set. NOTE: this does also mean that it will only run when provided with a String or Symbol.
21
+ * New config option: +Annotations::Config.valid_value_types+ - support for validation checks based on the class of the value object.
22
+ Example:
23
+ Annotations::Config::valid_value_types["tag"] = "Tag"
24
+
25
+ == 0.3.1
26
+ * Minor bugfixes
27
+ * +annotations_version_fu+ nows allows reloading of the versioned columns (needed if doing something during a migration).
28
+
29
+ == 0.3.0
30
+ * +acts_as_annotatable+ now requires you to specify an option - +:name_field+ - on the model that is becoming an annotatable.
31
+ * +acts_as_annotatable+ now exposes an +is_annotatable+ attribute to allow you to check if a model can act as an annotatable.
32
+
33
+ == 0.2.1
34
+ * Updated the routes to allow +requirements+ to be passed in.
35
+
36
+ == 0.2.0
37
+ *Main change*:
38
+ Annotation values are now polymorphic rather than just plain strings.
39
+ Some basic +act_as_annotation_value+ models have been introduced for this.
40
+ Note: this has affected all methods that take in or work with annotation values.
41
+ See below for further details.
42
+
43
+ * New mixin module: +acts_as_annotation_value+.
44
+ * New basic annotation value models: +TextValue+ and +NumberValue+ (but note that you can use any model as a value by specifying +acts_as_annotation_value+ on it).
45
+ * Removed +Annotation::find_annotatables_with_attribute_name_and_value+.
46
+ * Removed +Annotation::find_annotatables_with_attribute_names_and_values+.
47
+ * Removed +with_annotations_with_attribute_name_and_value+ in the +acts_as_annotatable+ module.
48
+ * +Annotations::Config::value_restrictions+ has been renamed to +Annotations::Config::content_restrictions+
49
+ * Latest migration version = v3
50
+ * NOTE: the new migration script will keep the old +value+ column data in a new +old_value+ column for EXISTING annotations only. This can be used for verification/text purposes.
51
+
52
+ == 0.1.1
53
+ * Added +identifier+ to +AnnotationAttribute+. This can be used to specify what ontology term / URI the attribute can be
54
+ uniquely identified using. See +AnnotationAttribute#before_validation+ for more information on how this identifier
55
+ will be generated if not specified manually.
56
+ * Changed the +annotations+ association in +act_as_annotation_source+ to +annotations_by+, to fix cases when a model has both
57
+ +acts_as_annotatable+ AND +acts_as_annotation_source+.
58
+ * Latest migration version = v2
59
+
60
+ == 0.1.0 (July 23rd 2009)
61
+ * Initial import from the BioCatalogue codebase.
62
+ * Improved documentation. See README.rdoc for more info on features and usage.
63
+ * Latest migration version = v1
64
+
data/INDEX.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = Annotations Plugin (for Ruby on Rails applications)
2
+
3
+ Original Author:: Jiten Bhagat (mailto:mail@jits.co.uk)
4
+ Copyright:: (c) 2008-2011, the University of Manchester and the European Bioinformatics Institute (EMBL-EBI)
5
+ License:: BSD
6
+ Version:: 0.4.1
7
+
8
+ For information on the plugin and to get started, see README.rdoc
9
+
10
+ For credits and origins, see AUTHORS.rdoc
11
+
12
+ For license, see LICENSE
13
+
14
+ For the latest updates, see CHANGELOG.rdoc
15
+
16
+ To run the tests in the plugin, see RUNNING_TESTS.rdoc
17
+
@@ -0,0 +1,32 @@
1
+ class AnnotationsMigrationGenerator < Rails::Generator::Base
2
+
3
+ attr_accessor :version
4
+
5
+ def initialize(*runtime_args)
6
+ super(*runtime_args)
7
+ if @args[0].nil?
8
+ @version = "all"
9
+ else
10
+ @version = @args[0].downcase
11
+ end
12
+ end
13
+
14
+ def manifest
15
+ record do |m|
16
+ if @version
17
+ if @version == "all"
18
+ Dir.chdir(File.join(File.dirname(__FILE__), "templates")) do
19
+ Dir.glob("*.rb").each do |f|
20
+ version = f.gsub(/.rb/, '').split('_')[1]
21
+ m.migration_template "migration_#{version}.rb", 'db/migrate', { :migration_file_name => "annotations_migration_#{version}" }
22
+ m.sleep 1 # So that the timestamps on the migration are not the same!
23
+ end
24
+ end
25
+ else
26
+ m.migration_template "migration_#{@version}.rb", 'db/migrate', { :migration_file_name => "annotations_migration_#{@version}" }
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,60 @@
1
+ class AnnotationsMigrationV1 < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :annotations, :force => true do |t|
4
+ t.string :source_type, :null => false
5
+ t.integer :source_id, :null => false
6
+ t.string :annotatable_type, :limit => 50, :null => false
7
+ t.integer :annotatable_id, :null => false
8
+ t.integer :attribute_id, :null => false
9
+ t.text :value, :limit => 20000, :null => false
10
+ t.string :value_type, :limit => 50, :null => false
11
+ t.integer :version, :null => false
12
+ t.integer :version_creator_id, :null => true
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :annotations, [ :source_type, :source_id ]
17
+ add_index :annotations, [ :annotatable_type, :annotatable_id ]
18
+ add_index :annotations, [ :attribute_id ]
19
+
20
+ create_table :annotation_versions, :force => true do |t|
21
+ t.integer :annotation_id, :null => false
22
+ t.integer :version, :null => false
23
+ t.integer :version_creator_id, :null => true
24
+ t.string :source_type, :null => false
25
+ t.integer :source_id, :null => false
26
+ t.string :annotatable_type, :limit => 50, :null => false
27
+ t.integer :annotatable_id, :null => false
28
+ t.integer :attribute_id, :null => false
29
+ t.text :value, :limit => 20000, :null => false
30
+ t.string :value_type, :limit => 50, :null => false
31
+ t.timestamps
32
+ end
33
+
34
+ add_index :annotation_versions, [ :annotation_id ]
35
+
36
+ create_table :annotation_attributes, :force => true do |t|
37
+ t.string :name, :null => false
38
+
39
+ t.timestamps
40
+ end
41
+
42
+ add_index :annotation_attributes, [ :name ]
43
+
44
+ create_table :annotation_value_seeds, :force => true do |t|
45
+ t.integer :attribute_id, :null => false
46
+ t.string :value, :null => false
47
+
48
+ t.timestamps
49
+ end
50
+
51
+ add_index :annotation_value_seeds, [ :attribute_id ]
52
+ end
53
+
54
+ def self.down
55
+ drop_table :annotations
56
+ drop_table :annotation_versions
57
+ drop_table :annotation_attributes
58
+ drop_table :annotation_value_seeds
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ class AnnotationsMigrationV2 < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :annotation_attributes, :identifier, :string, :null => false
4
+ end
5
+
6
+ def self.down
7
+ remove_column :annotation_attributes, :identifier
8
+ end
9
+ end
@@ -0,0 +1,74 @@
1
+ class AnnotationsMigrationV3 < ActiveRecord::Migration
2
+ def self.up
3
+
4
+ change_table :annotations do |t|
5
+ t.rename :value, :old_value
6
+ t.remove :value_type
7
+ t.string :value_type, :limit => 50, :null => false, :default => "TextValue"
8
+ t.integer :value_id, :null => false, :default => 0
9
+ end
10
+ change_column :annotations, :old_value, :string, :null => true
11
+ add_index :annotations, [ :value_type, :value_id ]
12
+
13
+ change_table :annotation_versions do |t|
14
+ t.rename :value, :old_value
15
+ t.remove :value_type
16
+ t.string :value_type, :limit => 50, :null => false, :default => "TextValue"
17
+ t.integer :value_id, :null => false, :default => 0
18
+ end
19
+ change_column :annotation_versions, :old_value, :string, :null => true
20
+
21
+ create_table :text_values, :force => true do |t|
22
+ t.integer :version, :null => false
23
+ t.integer :version_creator_id, :null => true
24
+ t.text :text, :limit => 16777214, :null => false
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :text_value_versions, :force => true do |t|
29
+ t.integer :text_value_id, :null => false
30
+ t.integer :version, :null => false
31
+ t.integer :version_creator_id, :null => true
32
+ t.text :text, :limit => 16777214, :null => false
33
+ t.timestamps
34
+ end
35
+ add_index :text_value_versions, [ :text_value_id ]
36
+
37
+ create_table :number_values, :force => true do |t|
38
+ t.integer :version, :null => false
39
+ t.integer :version_creator_id, :null => true
40
+ t.integer :number, :null => false
41
+ t.timestamps
42
+ end
43
+
44
+ create_table :number_value_versions, :force => true do |t|
45
+ t.integer :number_value_id, :null => false
46
+ t.integer :version, :null => false
47
+ t.integer :version_creator_id, :null => true
48
+ t.integer :number, :null => false
49
+ t.timestamps
50
+ end
51
+ add_index :number_value_versions, [ :number_value_id ]
52
+
53
+ # Migrate existing annotations to the v3 db schema
54
+ #
55
+ # TODO: IMPORTANT: please check the comments and logic in
56
+ # this util method to see if it is what you want.
57
+ # If you need to change the behaviour, redefine it in your app.
58
+ Annotation::reset_column_information
59
+ Annotation::reload_versioned_columns_info
60
+ Annotations::Util::migrate_annotations_to_v3
61
+
62
+ change_table :annotation_value_seeds do |t|
63
+ t.rename :value, :old_value
64
+ t.string :value_type, :limit => 50, :null => false, :default => "FIXME"
65
+ t.integer :value_id, :null => false, :default => 0
66
+ end
67
+ change_column :annotation_value_seeds, :old_value, :string, :null => true
68
+
69
+ end
70
+
71
+ def self.down
72
+ raise ActiveRecord::IrreversibleMigration.new
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ class AnnotationsMigrationV4 < ActiveRecord::Migration
2
+ def self.up
3
+ change_column :annotations,:version,:integer,:null=>true
4
+ change_column :text_values,:version,:integer,:null=>true
5
+ change_column :number_values,:version,:integer,:null=>true
6
+ end
7
+
8
+ def self.down
9
+ change_column :annotations,:version,:integer,:null=>false
10
+ change_column :text_values,:version,:integer,:null=>false
11
+ change_column :number_values,:version,:integer,:null=>false
12
+ end
13
+ end
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,271 @@
1
+ # ActsAsAnnotatable
2
+ module Annotations
3
+ module Acts #:nodoc:
4
+ module Annotatable #:nodoc:
5
+
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_annotatable(options)
12
+ cattr_accessor :annotatable_name_field, :is_annotatable
13
+
14
+ if options[:name_field].blank?
15
+ raise ArgumentError.new("Must specify the :name_field option that will be used as the field for the name")
16
+ end
17
+
18
+ self.annotatable_name_field = options[:name_field]
19
+
20
+ has_many :annotations,
21
+ :as => :annotatable,
22
+ :dependent => :destroy,
23
+ :order => 'updated_at ASC'
24
+
25
+ __send__ :extend, SingletonMethods
26
+ __send__ :include, InstanceMethods
27
+
28
+ self.is_annotatable = true
29
+ end
30
+ end
31
+
32
+ # Class methods added to the model that has been made acts_as_annotatable (ie: the mixin target class).
33
+ module SingletonMethods
34
+ # Helper finder to get all annotations for an object of the mixin annotatable type with the ID provided.
35
+ # This is the same as object.annotations with the added benefit that the object doesnt have to be loaded.
36
+ # E.g: Book.find_annotations_for(34) will give all annotations for the Book with ID 34.
37
+ def find_annotations_for(id, include_values=false)
38
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
39
+
40
+ options = {
41
+ :conditions => { :annotatable_type => obj_type,
42
+ :annotatable_id => id },
43
+ :order => "updated_at DESC"
44
+ }
45
+
46
+ options[:include] = [ :value ] if include_values
47
+
48
+ Annotation.find(:all, options)
49
+ end
50
+
51
+ # Helper finder to get all annotations for all objects of the mixin annotatable type, by the source specified.
52
+ # E.g: Book.find_annotations_by('User', 10) will give all annotations for all Books by User with ID 10.
53
+ def find_annotations_by(source_type, source_id, include_values=false)
54
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
55
+
56
+ options = {
57
+ :conditions => { :annotatable_type => obj_type,
58
+ :source_type => source_type,
59
+ :source_id => source_id },
60
+ :order => "updated_at DESC"
61
+ }
62
+
63
+ options[:include] = [ :value ] if include_values
64
+
65
+ Annotation.find(:all, options)
66
+ end
67
+ end
68
+
69
+ # This module contains instance methods
70
+ module InstanceMethods
71
+
72
+ # Gets the name of the annotatable object
73
+ def annotatable_name
74
+ self.send(self.class.annotatable_name_field)
75
+ end
76
+
77
+ # Helper method to get latest annotations
78
+ def latest_annotations(limit=nil, include_values=false)
79
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
80
+
81
+ options = {
82
+ :conditions => { :annotatable_type => obj_type,
83
+ :annotatable_id => self.id },
84
+ :order => "updated_at DESC",
85
+ :limit => limit
86
+ }
87
+
88
+ options[:include] = [ :value ] if include_values
89
+
90
+ Annotation.find(:all, options)
91
+ end
92
+
93
+ # Finder to get annotations with a specific attribute.
94
+ # The input parameter is the attribute name
95
+ # (MUST be a String representing the attribute's name).
96
+ def annotations_with_attribute(attrib, include_values=false)
97
+ return [] if attrib.blank?
98
+
99
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
100
+
101
+ options = {
102
+ :joins => :attribute,
103
+ :conditions => { :annotatable_type => obj_type,
104
+ :annotatable_id => self.id,
105
+ :annotation_attributes => { :name => attrib.strip.downcase } },
106
+ :order => "updated_at DESC"
107
+ }
108
+
109
+ options[:include] = [ :value ] if include_values
110
+
111
+ Annotation.find(:all, options)
112
+ end
113
+
114
+ # Same as the {obj}.annotations_with_attribute method (above) but
115
+ # takes in an array for attribute names to look for.
116
+ #
117
+ # NOTE (1): the argument to this method MUST be an Array of Strings.
118
+ def annotations_with_attributes(attribs, include_values=false)
119
+ return [] if attribs.blank?
120
+
121
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
122
+
123
+ options = {
124
+ :joins => :attribute,
125
+ :conditions => { :annotatable_type => obj_type,
126
+ :annotatable_id => self.id,
127
+ :annotation_attributes => { :name => attribs } },
128
+ :order => "updated_at DESC"
129
+ }
130
+
131
+ options[:include] = [ :value ] if include_values
132
+
133
+ Annotation.find(:all, options)
134
+ end
135
+
136
+ # Finder to get annotations with a specific attribute by a specific source.
137
+ #
138
+ # The first input parameter is the attribute name (MUST be a String representing the attribute's name).
139
+ # The second input is the source object.
140
+ def annotations_with_attribute_and_by_source(attrib, source, include_values=false)
141
+ return [] if attrib.blank? or source.nil?
142
+
143
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
144
+
145
+ options = {
146
+ :joins => :attribute,
147
+ :conditions => { :annotatable_type => obj_type,
148
+ :annotatable_id => self.id,
149
+ :source_type => source.class.name,
150
+ :source_id => source.id,
151
+ :annotation_attributes => { :name => attrib.strip.downcase } },
152
+ :order => "updated_at DESC"
153
+ }
154
+
155
+ options[:include] = [ :value ] if include_values
156
+
157
+ Annotation.find(:all, options)
158
+ end
159
+
160
+ # Finder to get all annotations on this object excluding those that
161
+ # have the attribute names specified.
162
+ #
163
+ # NOTE (1): the argument to this method MUST be an Array of Strings.
164
+ # NOTE (2): the returned records will be Read Only.
165
+ def all_annotations_excluding_attributes(attribs, include_values=false)
166
+ return [] if attribs.blank?
167
+
168
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
169
+
170
+ options = {
171
+ :joins => :attribute,
172
+ :conditions => [ "`annotations`.`annotatable_type` = ? AND `annotations`.`annotatable_id` = ? AND `annotation_attributes`.`name` NOT IN (?)",
173
+ obj_type,
174
+ self.id,
175
+ attribs ],
176
+ :order => "`annotations`.`updated_at` DESC"
177
+ }
178
+
179
+ options[:include] = [ :value ] if include_values
180
+
181
+ Annotation.find(:all, options)
182
+ end
183
+
184
+ # Returns the number of annotations on this annotatable object by the source type specified.
185
+ # "all" (case insensitive) can be provided to get all annotations regardless of source type.
186
+ # E.g.: book.count_annotations_by("User") or book.count_annotations_by("All")
187
+ def count_annotations_by(source_type_in)
188
+ if source_type_in == nil || source_type_in.downcase == "all"
189
+ return self.annotations.count
190
+ else
191
+ return self.annotations.count(:conditions => { :source_type => source_type_in })
192
+ end
193
+ end
194
+
195
+ # Use this method to create many annotations from a Hash of data.
196
+ # Arrays for Hash values will be converted to multiple annotations.
197
+ # Blank values (nil or empty string) will be ignored and thus annotations
198
+ # will not be created for them.
199
+ #
200
+ # Returns an array of Annotation objects of the annotations that were
201
+ # successfully created.
202
+ #
203
+ # Code example:
204
+ # -------------
205
+ # data = { "tag" => [ "tag1", "tag2", "tag3" ], "description" => "This is a book" }
206
+ # book.create_annotations(data, current_user)
207
+ def create_annotations(annotations_data, source)
208
+ anns = [ ]
209
+
210
+ annotations_data.each do |attrib, val|
211
+ unless val.blank?
212
+ val = [ val ].flatten
213
+ val.each do |val_inner|
214
+ unless val_inner.blank?
215
+ ann = self.annotations.new(:attribute_name => attrib,
216
+ :source_type => source.class.name,
217
+ :source_id => source.id)
218
+
219
+ ann.value = val_inner
220
+ ann.save
221
+
222
+ if ann && ann.valid?
223
+ anns << ann
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ # Reload annotations collection
231
+ self.annotations(true)
232
+
233
+ return anns
234
+ end
235
+
236
+ # When used with the default style (:simple), returns a Hash of the +annotations+ values
237
+ # grouped by attribute name.
238
+ #
239
+ # Example output:
240
+ # {
241
+ # "Summary" => "Something interesting happens",
242
+ # "length" => 345,
243
+ # "Title" => "Harry Potter and the Exploding Men's Locker Room",
244
+ # "Tag" => [ "amusing rhetoric", "wizadry" ],
245
+ # "rating" => "4/5"
246
+ # }
247
+ def annotations_hash(style=:simple)
248
+ h = { }
249
+
250
+ unless self.annotations.blank?
251
+ self.annotations.each do |a|
252
+ if h.has_key?(a.attribute_name)
253
+ case h[a.attribute_name]
254
+ when Array
255
+ h[a.attribute_name] << a.value_content
256
+ else
257
+ h[a.attribute_name] = [ h[a.attribute_name], a.value_content ]
258
+ end
259
+ else
260
+ h[a.attribute_name] = a.value_content
261
+ end
262
+ end
263
+ end
264
+
265
+ return h
266
+ end
267
+ end
268
+
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,117 @@
1
+ # ActsAsAnnotationSource
2
+ module Annotations
3
+ module Acts #:nodoc:
4
+ module AnnotationSource #:nodoc:
5
+
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_annotation_source
12
+ has_many :annotations_by,
13
+ :class_name => "Annotation",
14
+ :as => :source,
15
+ :order => 'updated_at ASC'
16
+
17
+ __send__ :extend, SingletonMethods
18
+ __send__ :include, InstanceMethods
19
+ end
20
+ end
21
+
22
+ # Class methods added to the model that has been made acts_as_annotation_source (the mixin target class).
23
+ module SingletonMethods
24
+ # Helper finder to get all annotations for an object of the mixin source type with the ID provided.
25
+ # This is the same as +#annotations+ on the object, with the added benefit that the object doesnt have to be loaded.
26
+ # E.g: +User.find_annotations_by(10)+ will give all annotations by User with ID 34.
27
+ def annotations_by(id, include_values=false)
28
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
29
+
30
+ options = {
31
+ :conditions => { :source_type => obj_type,
32
+ :source_id => id },
33
+ :order => "updated_at DESC"
34
+ }
35
+
36
+ options[:include] = [ :value ] if include_values
37
+
38
+ Annotation.find(:all, options)
39
+ end
40
+
41
+ # Helper finder to get all annotations for all objects of the mixin source type, for the annotatable object provided.
42
+ # E.g: +User.find_annotations_for('Book', 28)+ will give all annotations made by all Users for Book with ID 28.
43
+ def annotations_for(annotatable_type, annotatable_id, include_values=false)
44
+ obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
45
+
46
+ options = {
47
+ :conditions => { :source_type => obj_type,
48
+ :annotatable_type => annotatable_type,
49
+ :annotatable_id => annotatable_id },
50
+ :order => "updated_at DESC"
51
+ }
52
+
53
+ options[:include] = [ :value ] if include_values
54
+
55
+ Annotation.find(:all, options)
56
+ end
57
+ end
58
+
59
+ # This module contains instance methods
60
+ module InstanceMethods
61
+ # Helper method to get latest annotations
62
+ def latest_annotations(limit=nil, include_values=false)
63
+ options = {
64
+ :conditions => { :source_type => self.class.name,
65
+ :source_id => id },
66
+ :order => "updated_at DESC",
67
+ :limit => limit
68
+ }
69
+
70
+ options[:include] = [ :value ] if include_values
71
+
72
+ Annotation.find(:all, options)
73
+ end
74
+
75
+ def annotation_source_name
76
+ %w{ preferred_name display_name title name }.each do |w|
77
+ return eval("self.#{w}") if self.respond_to?(w)
78
+ end
79
+ return "#{self.class.name}_#{self.id}"
80
+ end
81
+
82
+ # When used with the default style (:simple), returns a Hash of the +annotations_by+ values
83
+ # grouped by attribute name.
84
+ #
85
+ # Example output:
86
+ # {
87
+ # "Summary" => "Something interesting happens",
88
+ # "length" => 345,
89
+ # "Title" => "Harry Potter and the Exploding Men's Locker Room",
90
+ # "Tag" => [ "amusing rhetoric", "wizadry" ],
91
+ # "rating" => "4/5"
92
+ # }
93
+ def annotations_by_hash(style=:simple)
94
+ h = { }
95
+
96
+ unless self.annotations_by.blank?
97
+ self.annotations_by.each do |a|
98
+ if h.has_key?(a.attribute_name)
99
+ case h[a.attribute_name]
100
+ when Array
101
+ h[a.attribute_name] << a.value_content
102
+ else
103
+ h[a.attribute_name] = [ h[a.attribute_name], a.value_content ]
104
+ end
105
+ else
106
+ h[a.attribute_name] = a.value_content
107
+ end
108
+ end
109
+ end
110
+
111
+ return h
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+ end