Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial commit - imported the annotations plugin from the BioCatalogu…

…e codebase
  • Loading branch information...
commit 5b50d473d65447630f8a9f4f6d7500f72f1f6593 0 parents
Jiten Bhagat authored July 23, 2009

Showing 57 changed files with 2,854 additions and 0 deletions. Show diff stats Hide diff stats

  1. 2  CHANGELOG
  2. 20  MIT-LICENSE
  3. 9  README
  4. 4  RUNNING_UNIT_TESTS
  5. 22  RakeFile
  6. 11  generators/annotations_migration/annotations_migration_generator.rb
  7. 60  generators/annotations_migration/templates/migration.rb
  8. 1  init.rb
  9. 1  install.rb
  10. 18  lib/annotations.rb
  11. 220  lib/annotations/acts_as_annotatable.rb
  12. 69  lib/annotations/acts_as_annotation_source.rb
  13. 95  lib/annotations/config.rb
  14. 7  lib/annotations/routing.rb
  15. 110  lib/annotations_version_fu.rb
  16. 162  lib/app/controllers/annotations_controller.rb
  17. 2  lib/app/controllers/application_controller.rb
  18. 2  lib/app/helpers/application_helper.rb
  19. 329  lib/app/models/annotation.rb
  20. 9  lib/app/models/annotation_attribute.rb
  21. 14  lib/app/models/annotation_value_seed.rb
  22. 7  script/console
  23. 4  tasks/annotations_tasks.rake
  24. 119  test/acts_as_annotatable_test.rb
  25. 56  test/acts_as_annotation_source_test.rb
  26. 22  test/annotation_attribute_test.rb
  27. 168  test/annotation_test.rb
  28. 14  test/annotation_value_seed_test.rb
  29. 33  test/annotation_version_test.rb
  30. 27  test/annotations_controller_test.rb
  31. 9  test/app_root/app/controllers/application_controller.rb
  32. 5  test/app_root/app/models/book.rb
  33. 5  test/app_root/app/models/chapter.rb
  34. 3  test/app_root/app/models/group.rb
  35. 3  test/app_root/app/models/user.rb
  36. 12  test/app_root/app/views/annotations/edit.html.erb
  37. 1  test/app_root/app/views/annotations/index.html.erb
  38. 11  test/app_root/app/views/annotations/new.html.erb
  39. 3  test/app_root/app/views/annotations/show.html.erb
  40. 115  test/app_root/config/boot.rb
  41. 6  test/app_root/config/database.yml
  42. 16  test/app_root/config/environment.rb
  43. 0  test/app_root/config/environments/mysql.rb
  44. 4  test/app_root/config/routes.rb
  45. 33  test/app_root/db/migrate/001_create_test_models.rb
  46. 60  test/app_root/db/migrate/002_annotations_migration.rb
  47. 279  test/config_test.rb
  48. 39  test/fixtures/annotation_attributes.yml
  49. 16  test/fixtures/annotation_value_seeds.csv
  50. 259  test/fixtures/annotation_versions.yml
  51. 239  test/fixtures/annotations.yml
  52. 13  test/fixtures/books.yml
  53. 27  test/fixtures/chapters.yml
  54. 7  test/fixtures/groups.yml
  55. 8  test/fixtures/users.yml
  56. 27  test/routing_test.rb
  57. 37  test/test_helper.rb
2  CHANGELOG
... ...
@@ -0,0 +1,2 @@
  1
+
  2
+              
20  MIT-LICENSE
... ...
@@ -0,0 +1,20 @@
  1
+Copyright (c) 2008 BioCatalogue
  2
+
  3
+Permission is hereby granted, free of charge, to any person obtaining
  4
+a copy of this software and associated documentation files (the
  5
+"Software"), to deal in the Software without restriction, including
  6
+without limitation the rights to use, copy, modify, merge, publish,
  7
+distribute, sublicense, and/or sell copies of the Software, and to
  8
+permit persons to whom the Software is furnished to do so, subject to
  9
+the following conditions:
  10
+
  11
+The above copyright notice and this permission notice shall be
  12
+included in all copies or substantial portions of the Software.
  13
+
  14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9  README
... ...
@@ -0,0 +1,9 @@
  1
+Annotations
  2
+===========
  3
+
  4
+Allows for annotations to be added to multiple and different models.
  5
+
  6
+KNOWN ISSUES:
  7
+=============
  8
+
  9
+- Will not work with sqlite and sqlite3 databases. Furthermore, not tested with Postgres.
4  RUNNING_UNIT_TESTS
... ...
@@ -0,0 +1,4 @@
  1
+To run the tests for this plugin:
  2
+- ensure the gem 'plugin_test_helper' is installed (only tested with v0.3.0).
  3
+- ensure database 'annotations_plugin_test' exists on a local mysql server installation (with default ports) and username 'root' with blank password.
  4
+- run: rake test:plugins PLUGIN=annotations
22  RakeFile
... ...
@@ -0,0 +1,22 @@
  1
+require 'rake'
  2
+require 'rake/testtask'
  3
+require 'rake/rdoctask'
  4
+
  5
+desc 'Default: run unit tests.'
  6
+task :default => :test
  7
+
  8
+desc 'Test the annotations plugin.'
  9
+Rake::TestTask.new(:test) do |t|
  10
+  t.libs << 'lib'
  11
+  t.pattern = 'test/**/*_test.rb'
  12
+  t.verbose = true
  13
+end
  14
+
  15
+desc 'Generate documentation for the annotations plugin.'
  16
+Rake::RDocTask.new(:rdoc) do |rdoc|
  17
+  rdoc.rdoc_dir = 'rdoc'
  18
+  rdoc.title    = 'Annotations plugin'
  19
+  rdoc.options << '--line-numbers' << '--inline-source'
  20
+  rdoc.rdoc_files.include('README')
  21
+  rdoc.rdoc_files.include('lib/**/*.rb')
  22
+end
11  generators/annotations_migration/annotations_migration_generator.rb
... ...
@@ -0,0 +1,11 @@
  1
+class AnnotationsMigrationGenerator < Rails::Generator::Base 
  2
+  def manifest 
  3
+    record do |m| 
  4
+      m.migration_template 'migration.rb', 'db/migrate' 
  5
+    end 
  6
+  end
  7
+  
  8
+  def file_name
  9
+    "annotations_migration"
  10
+  end
  11
+end
60  generators/annotations_migration/templates/migration.rb
... ...
@@ -0,0 +1,60 @@
  1
+class AnnotationsMigration < 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
1  init.rb
... ...
@@ -0,0 +1 @@
  1
+require File.join(File.dirname(__FILE__), "lib", "annotations")
1  install.rb
... ...
@@ -0,0 +1 @@
  1
+# Install hook code here
18  lib/annotations.rb
... ...
@@ -0,0 +1,18 @@
  1
+require File.join(File.dirname(__FILE__), "annotations", "config")
  2
+
  3
+require File.join(File.dirname(__FILE__), "annotations_version_fu")
  4
+
  5
+%w{ models controllers helpers }.each do |dir|
  6
+  path = File.join(File.dirname(__FILE__), 'app', dir)
  7
+  $LOAD_PATH << path
  8
+  ActiveSupport::Dependencies.load_paths << path
  9
+  ActiveSupport::Dependencies.load_once_paths.delete(path)
  10
+end
  11
+
  12
+require File.join(File.dirname(__FILE__), "annotations", "acts_as_annotatable")
  13
+ActiveRecord::Base.send(:include, Annotations::Acts::Annotatable)
  14
+
  15
+require File.join(File.dirname(__FILE__), "annotations", "acts_as_annotation_source")
  16
+ActiveRecord::Base.send(:include, Annotations::Acts::AnnotationSource)
  17
+
  18
+require File.join(File.dirname(__FILE__), "annotations", "routing")
220  lib/annotations/acts_as_annotatable.rb
... ...
@@ -0,0 +1,220 @@
  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
  12
+          has_many :annotations, 
  13
+                   :as => :annotatable, 
  14
+                   :dependent => :destroy, 
  15
+                   :order => 'created_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_annotatable (ie: the mixin annotatable type).
  23
+      module SingletonMethods
  24
+        # Helper finder to get all objects of the mixin annotatable type that have the specified attribute name and value.
  25
+        #
  26
+        # NOTE (1): both the attribute name and the value will be treated case insensitively.
  27
+        def with_annotations_with_attribute_name_and_value(attribute_name, value)
  28
+          return [ ] if attribute_name.blank? or value.nil?
  29
+          
  30
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  31
+          
  32
+          anns = Annotation.find(:all,
  33
+                                 :joins => :attribute,
  34
+                                 :conditions => { :annotatable_type => obj_type, 
  35
+                                                  :annotation_attributes =>  { :name => attribute_name.strip.downcase }, 
  36
+                                                  :value => value.strip.downcase })
  37
+                                                  
  38
+          return anns.map{|a| a.annotatable}
  39
+        end
  40
+        
  41
+        # Helper finder to get all annotations for an object of the mixin annotatable type with the ID provided.
  42
+        # This is the same as object.annotations with the added benefit that the object doesnt have to be loaded.
  43
+        # E.g: Book.find_annotations_for(34) will give all annotations for the Book with ID 34.
  44
+        def find_annotations_for(id)
  45
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  46
+          
  47
+          Annotation.find(:all,
  48
+                          :conditions => { :annotatable_type =>  obj_type, 
  49
+                                           :annotatable_id => id },
  50
+                          :order => "created_at DESC")
  51
+        end
  52
+        
  53
+        # Helper finder to get all annotations for all objects of the mixin annotatable type, by the source specified.
  54
+        # E.g: Book.find_annotations_by('User', 10) will give all annotations for all Books by User with ID 10. 
  55
+        def find_annotations_by(source_type, source_id)
  56
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  57
+          
  58
+          Annotation.find(:all,
  59
+                          :conditions => { :annotatable_type =>  obj_type, 
  60
+                                           :source_type => source_type,
  61
+                                           :source_id => source_id },
  62
+                          :order => "created_at DESC")
  63
+        end
  64
+      end
  65
+      
  66
+      # This module contains instance methods
  67
+      module InstanceMethods
  68
+        
  69
+        # Provides a default implementation to get the display name for 
  70
+        # an annotatable object, that can be overrided.
  71
+        def annotatable_name
  72
+          %w{ display_name title name }.each do |w|
  73
+            return eval("self.#{w}") if self.respond_to?(w)
  74
+          end
  75
+          return "#{self.class.name}_#{id}"
  76
+        end
  77
+        
  78
+        # Helper method to get latest annotations
  79
+        def latest_annotations(limit=nil)
  80
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
  81
+          
  82
+          Annotation.find(:all,
  83
+                          :conditions => { :annotatable_type =>  obj_type, 
  84
+                                           :annotatable_id => self.id },
  85
+                          :order => "created_at DESC",
  86
+                          :limit => limit)
  87
+        end
  88
+        
  89
+        # Finder to get annotations with a specific attribute.
  90
+        # The input parameter is the attribute name 
  91
+        # (MUST be a String representing the attribute's name).
  92
+        def annotations_with_attribute(attrib)
  93
+          return [] if attrib.blank?
  94
+          
  95
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
  96
+          
  97
+          Annotation.find(:all,
  98
+                          :joins => :attribute,
  99
+                          :conditions => { :annotatable_type => obj_type,
  100
+                                           :annotatable_id => self.id,
  101
+                                           :annotation_attributes =>  { :name => attrib.strip.downcase } },
  102
+                          :order => "created_at DESC")
  103
+        end
  104
+        
  105
+        # Same as the {obj}.annotations_with_attribute method (above) but 
  106
+        # takes in an array for attribute names to look for.
  107
+        #
  108
+        # NOTE (1): the argument to this method MUST be an Array of Strings.
  109
+        def annotations_with_attributes(attribs)
  110
+          return [] if attribs.blank?
  111
+          
  112
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
  113
+          
  114
+          Annotation.find(:all,
  115
+                          :joins => :attribute,
  116
+                          :conditions => { :annotatable_type => obj_type,
  117
+                                           :annotatable_id => self.id,
  118
+                                           :annotation_attributes =>  { :name => attribs } },
  119
+                          :order => "created_at DESC")
  120
+        end
  121
+        
  122
+        # Finder to get annotations with a specific attribute by a specific source.
  123
+        #
  124
+        # The first input parameter is the attribute name (MUST be a String representing the attribute's name).
  125
+        # The second input is the source object.
  126
+        def annotations_with_attribute_and_by_source(attrib, source)
  127
+          return [] if attrib.blank? or source.nil?
  128
+          
  129
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
  130
+          
  131
+          Annotation.find(:all,
  132
+                          :joins => :attribute,
  133
+                          :conditions => { :annotatable_type => obj_type,
  134
+                                           :annotatable_id => self.id,
  135
+                                           :source_type => source.class.name,
  136
+                                           :source_id => source.id,
  137
+                                           :annotation_attributes =>  { :name => attrib.strip.downcase } },
  138
+                          :order => "created_at DESC")
  139
+        end
  140
+        
  141
+        # Finder to get all annotations on this object excluding those that
  142
+        # have the attribute names specified.
  143
+        #
  144
+        # NOTE (1): the argument to this method MUST be an Array of Strings.
  145
+        # NOTE (2): the returned records will be Read Only.
  146
+        def all_annotations_excluding_attributes(attribs)
  147
+          return [] if attribs.blank?
  148
+          
  149
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self.class).to_s
  150
+          
  151
+          Annotation.find(:all,
  152
+                          :joins => :attribute,
  153
+                          :conditions => [ "`annotations`.`annotatable_type` = ? AND `annotations`.`annotatable_id` = ? AND `annotation_attributes`.`name` NOT IN (?)",
  154
+                                           obj_type,
  155
+                                           self.id,
  156
+                                           attribs ],
  157
+                          :order => "`annotations`.`created_at` DESC")
  158
+        end
  159
+        
  160
+        # Returns the number of annotations on this annotatable object by the source type specified.
  161
+        # "all" (case insensitive) can be provided to get all annotations regardless of source type.
  162
+        # E.g.: book.count_annotations_by("User") or book.count_annotations_by("All")
  163
+        def count_annotations_by(source_type_in)
  164
+          if source_type_in == nil || source_type_in.downcase == "all"
  165
+            return self.annotations.count
  166
+          else
  167
+            return self.annotations.count(:conditions => { :source_type => source_type_in })  
  168
+          end
  169
+        end
  170
+        
  171
+        # Use this method to create many annotations from a Hash of data.
  172
+        # Arrays for Hash values will be converted to multiple annotations.
  173
+        # Blank values (nil or empty string) will be ignored and thus annotations
  174
+        # will not be created for them.
  175
+        #
  176
+        # Returns an array of Annotation objects of the annotations that were
  177
+        # successfully created.
  178
+        #
  179
+        # Code example:
  180
+        # -------------
  181
+        # data = { "tag" => [ "tag1", "tag2", "tag3" ], "description" => "This is a book" }
  182
+        # book.create_annotations(data, current_user)
  183
+        def create_annotations(annotations_data, source)
  184
+          anns = [ ]
  185
+          
  186
+          annotations_data.each do |attrib, val|
  187
+            unless val.blank?
  188
+              if val.is_a? Array
  189
+                val.each do |val_inner|
  190
+                  unless val_inner.blank?
  191
+                    ann = self.annotations << Annotation.new(:attribute_name => attrib, 
  192
+                                                 :value => val_inner, 
  193
+                                                 :source_type => source.class.name, 
  194
+                                                 :source_id => source.id)
  195
+                    
  196
+                    unless ann.nil? || ann == false
  197
+                      anns << ann
  198
+                    end
  199
+                  end
  200
+                end
  201
+              else
  202
+                ann = self.annotations << Annotation.new(:attribute_name => attrib, 
  203
+                                             :value => val, 
  204
+                                             :source_type => source.class.name, 
  205
+                                             :source_id => source.id)
  206
+                
  207
+                unless ann.nil? || ann == false
  208
+                  anns << ann
  209
+                end
  210
+              end
  211
+            end
  212
+          end
  213
+          
  214
+          return anns
  215
+        end
  216
+      end
  217
+      
  218
+    end
  219
+  end
  220
+end
69  lib/annotations/acts_as_annotation_source.rb
... ...
@@ -0,0 +1,69 @@
  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, 
  13
+                   :as => :source, 
  14
+                   :order => 'created_at ASC'
  15
+                   
  16
+          send :extend, SingletonMethods
  17
+          send :include, InstanceMethods
  18
+        end
  19
+      end
  20
+      
  21
+      # Class methods added to the model that has been made acts_as_annotation_source (the mixin source type).
  22
+      module SingletonMethods
  23
+        # Helper finder to get all annotations for an object of the mixin source type with the ID provided.
  24
+        # This is the same as object.annotations with the added benefit that the object doesnt have to be loaded.
  25
+        # E.g: User.find_annotations_by(10) will give all annotations by User with ID 34.
  26
+        def annotations_by(id)
  27
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  28
+          
  29
+          Annotation.find(:all,
  30
+                          :conditions => { :source_type =>  obj_type, 
  31
+                                           :source_id => id },
  32
+                          :order => "created_at DESC")
  33
+        end
  34
+        
  35
+        # Helper finder to get all annotations for all objects of the mixin source type, for the annotatable object provided.
  36
+        # E.g: User.find_annotations_for('Book', 28) will give all annotations made by all Users for Book with ID 28. 
  37
+        def annotations_for(annotatable_type, annotatable_id)
  38
+          obj_type = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  39
+          
  40
+          Annotation.find(:all,
  41
+                          :conditions => { :source_type => obj_type,
  42
+                                           :annotatable_type =>  annotatable_type, 
  43
+                                           :annotatable_id => annotatable_id },
  44
+                          :order => "created_at DESC")
  45
+        end
  46
+      end
  47
+      
  48
+      # This module contains instance methods
  49
+      module InstanceMethods
  50
+        # Helper method to get latest annotations
  51
+        def latest_annotations(limit=nil)
  52
+          Annotation.find(:all,
  53
+                          :conditions => { :source_type =>  self.class.name, 
  54
+                                           :source_id => id },
  55
+                          :order => "created_at DESC",
  56
+                          :limit => limit)
  57
+        end
  58
+        
  59
+        def annotation_source_name
  60
+          %w{ display_name title name }.each do |w|
  61
+            return eval("self.#{w}") if self.respond_to?(w)
  62
+          end
  63
+          return "#{self.class.name}_#{id}"
  64
+        end
  65
+      end
  66
+      
  67
+    end
  68
+  end
  69
+end
95  lib/annotations/config.rb
... ...
@@ -0,0 +1,95 @@
  1
+module Annotations
  2
+  module Config
  3
+    # List of attribute name(s) that need the corresponding value to be downcased (made all lowercase).
  4
+    # 
  5
+    # NOTE: The attribute names specified MUST all be in lowercase.
  6
+    @@attribute_names_for_values_to_be_downcased = [ ]
  7
+    
  8
+    # List of attribute name(s) that need the corresponding value to be upcased (made all uppercase).
  9
+    #
  10
+    # NOTE: The attribute names specified MUST all be in lowercase.
  11
+    @@attribute_names_for_values_to_be_upcased = [ ]
  12
+    
  13
+    # This defines a hash of attributes, and the characters/strings that need to be stripped (removed) out of values of the attributes specified.
  14
+    # Regular expressions can also be used instead of characters/strings.
  15
+    # ie: { attribute_name => [ array of characters to strip out ] }    (note: doesn't have to be an array, can be a single string)
  16
+    #
  17
+    # e.g: { "tag" => [ '"', ','] } or { "tag" => '"' }
  18
+    # 
  19
+    # NOTE: The attribute name(s) specified MUST all be in lowercase.  
  20
+    @@strip_text_rules = { }
  21
+    
  22
+    # This allows you to specify a different model name for users in the system (if different from the default: "User").
  23
+    @@user_model_name = "User"
  24
+    
  25
+    # This allows you to limit the number of annotations (of specified attribute names) per source per annotatable.
  26
+    #
  27
+    # Key/value pairs in hash should follow the spec:
  28
+    # { attribute_name => max_number_allowed }
  29
+    #
  30
+    # e.g: { "rating" =>1 } - will only ever allow 1 "rating" annotation per annotatable by each source.
  31
+    #
  32
+    # NOTE (1): The attribute name(s) specified MUST all be in lowercase.
  33
+    @@limits_per_source = { }
  34
+    
  35
+    # By default, duplicate annotations CANNOT be created (same value for the same attribute, on the same annotatable object, regardless of source). 
  36
+    # For example: a user cannot add a description to a specific book that matches an existing description for that book.
  37
+    # 
  38
+    # This config setting allows exceptions to this rule, on a per attribute basis. 
  39
+    # I.e: allow annotations with certain attribute names to have duplicate values (per annotatable).
  40
+    #
  41
+    # e.g: [ "tag", "rating" ] - allows tags and ratings to have the same value more than once.
  42
+    #
  43
+    # NOTE (1): The attribute name(s) specified MUST all be in lowercase.
  44
+    # NOTE (2): This setting can be used in conjunction with the limits_per_source setting to allow 
  45
+    #           duplicate annotations BUT limit the number of annotations (per attribute) per user.
  46
+    @@attribute_names_to_allow_duplicates = [ ]
  47
+    
  48
+    # This allows you to restrict the value for annotations with a specific attribute name.
  49
+    #
  50
+    # Key/value pairs in the hash should follow the spec:
  51
+    # { attribute_name => { :in => array_or_range, :error_message => error_msg_to_show_if_value_not_allowed }
  52
+    #
  53
+    # e.g: { "rating" => { :in => 1..5, :error_message => "Please provide a rating between 1 and 5" } }
  54
+    #
  55
+    # NOTE (1): The attribute name(s) specified MUST all be in lowercase.
  56
+    # NOTE (2): values will be checked in a case insensitive manner.
  57
+    @@value_restrictions = { }
  58
+    
  59
+    def self.reset
  60
+      @@attribute_names_for_values_to_be_downcased = [ ]
  61
+      @@attribute_names_for_values_to_be_upcased = [ ]
  62
+      @@strip_text_rules = { }
  63
+      @@user_model_name = "User"
  64
+      @@limits_per_source = { }
  65
+      @@attribute_names_to_allow_duplicates = [ ]
  66
+      @@value_restrictions = { }
  67
+    end
  68
+    
  69
+    reset
  70
+    
  71
+    # This makes the variables above available externally.
  72
+    # Shamelessly borrowed from the GeoKit plugin.
  73
+    [ :attribute_names_for_values_to_be_downcased,
  74
+      :attribute_names_for_values_to_be_upcased,
  75
+      :strip_text_rules,
  76
+      :user_model_name,
  77
+      :limits_per_source,
  78
+      :attribute_names_to_allow_duplicates,
  79
+      :value_restrictions ].each do |sym|
  80
+      class_eval <<-EOS, __FILE__, __LINE__
  81
+        def self.#{sym}
  82
+          if defined?(#{sym.to_s.upcase})
  83
+            #{sym.to_s.upcase}
  84
+          else
  85
+            @@#{sym}
  86
+          end
  87
+        end
  88
+
  89
+        def self.#{sym}=(obj)
  90
+          @@#{sym} = obj
  91
+        end
  92
+      EOS
  93
+    end
  94
+  end
  95
+end
7  lib/annotations/routing.rb
... ...
@@ -0,0 +1,7 @@
  1
+module Annotations #:nodoc:
  2
+  def self.map_routes(map, collection={}, member={})
  3
+    map.resources :annotations,
  4
+                  :collection => { :create_multiple => :post }.merge(collection),
  5
+                  :member => {}.merge(member)
  6
+  end
  7
+end
110  lib/annotations_version_fu.rb
... ...
@@ -0,0 +1,110 @@
  1
+# Kindly taken from the version_fu plugin (http://github.com/jmckible/version_fu/tree/master)
  2
+
  3
+# Module and file renamed (and modified accordingly) on 2009-01-28 by Jits,
  4
+# to prevent conflicts with an external version_fu plugin installed in the main codebase.
  5
+
  6
+module AnnotationsVersionFu
  7
+  def self.included(base)
  8
+    base.extend ClassMethods
  9
+  end
  10
+
  11
+  module ClassMethods
  12
+    def annotations_version_fu(options={}, &block)
  13
+      return if self.included_modules.include? AnnotationsVersionFu::InstanceMethods
  14
+      __send__ :include, AnnotationsVersionFu::InstanceMethods
  15
+
  16
+      cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, 
  17
+                     :version_column, :versioned_columns
  18
+
  19
+      self.versioned_class_name         = options[:class_name]  || 'Version'
  20
+      self.versioned_foreign_key        = options[:foreign_key] || self.to_s.foreign_key
  21
+      self.versioned_table_name         = options[:table_name]  || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
  22
+      self.version_column               = options[:version_column]    || 'version'
  23
+
  24
+      # Setup versions association
  25
+      class_eval do
  26
+        has_many :versions, :class_name  => "#{self.to_s}::#{versioned_class_name}",
  27
+                            :foreign_key => versioned_foreign_key,
  28
+                            :dependent   => :destroy do
  29
+          def latest
  30
+            find :first, :order=>'version desc'
  31
+          end                    
  32
+        end
  33
+
  34
+        before_save :check_for_new_version
  35
+      end
  36
+      
  37
+      # Versioned Model
  38
+      const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
  39
+        # find first version before the given version
  40
+        def self.before(version)
  41
+          find :first, :order => 'version desc',
  42
+            :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
  43
+        end
  44
+
  45
+        # find first version after the given version.
  46
+        def self.after(version)
  47
+          find :first, :order => 'version',
  48
+            :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
  49
+        end
  50
+
  51
+        def previous
  52
+          self.class.before(self)
  53
+        end
  54
+
  55
+        def next
  56
+          self.class.after(self)
  57
+        end
  58
+      end
  59
+
  60
+      # Housekeeping on versioned class
  61
+      versioned_class.cattr_accessor :original_class
  62
+      versioned_class.original_class = self
  63
+      versioned_class.set_table_name versioned_table_name
  64
+      
  65
+      # Version parent association
  66
+      versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym, 
  67
+        :class_name  => "::#{self.to_s}", 
  68
+        :foreign_key => versioned_foreign_key
  69
+      
  70
+      # Block extension
  71
+      versioned_class.class_eval &block if block_given?
  72
+      
  73
+      # Finally setup which columns to version
  74
+      self.versioned_columns =  versioned_class.new.attributes.keys - 
  75
+        [versioned_class.primary_key, versioned_foreign_key, version_column, 'created_at', 'updated_at']
  76
+    end
  77
+    
  78
+    def versioned_class
  79
+      const_get versioned_class_name
  80
+    end
  81
+  end
  82
+
  83
+
  84
+  module InstanceMethods
  85
+    def find_version(number)
  86
+      versions.find :first, :conditions=>{:version=>number}
  87
+    end
  88
+    
  89
+    def check_for_new_version
  90
+      instatiate_revision if create_new_version?
  91
+      true # Never halt save
  92
+    end
  93
+    
  94
+    # This the method to override if you want to have more control over when to version
  95
+    def create_new_version?
  96
+      # Any versioned column changed?
  97
+      self.class.versioned_columns.detect {|a| __send__ "#{a}_changed?"}
  98
+    end
  99
+    
  100
+    def instatiate_revision
  101
+      new_version = versions.build
  102
+      versioned_columns.each do |attribute|
  103
+        new_version.__send__ "#{attribute}=", __send__(attribute)
  104
+      end
  105
+      version_number = new_record? ? 1 : version + 1
  106
+      new_version.version = version_number
  107
+      self.version = version_number
  108
+    end
  109
+  end
  110
+end
162  lib/app/controllers/annotations_controller.rb
... ...
@@ -0,0 +1,162 @@
  1
+class AnnotationsController < ApplicationController
  2
+  
  3
+  before_filter :login_required, :only => [ :new, :create, :edit, :update, :destroy ]
  4
+  
  5
+  before_filter :find_annotation, :only => [ :show, :edit, :update, :destroy ] 
  6
+  before_filter :find_annotatable, :except => [ :show, :edit, :update, :destroy ]
  7
+  before_filter :authorise_action, :only =>  [ :edit, :update, :destroy ]
  8
+  
  9
+  # GET /annotations
  10
+  # GET /annotations.xml
  11
+  def index
  12
+    params[:num] ||= 50
  13
+    
  14
+    @annotations =  
  15
+    if @annotatable.nil?
  16
+      Annotation.find(:all, :limit => params[:num])      
  17
+    else
  18
+      @annotatable.latest_annotations(params[:num])
  19
+    end
  20
+
  21
+    respond_to do |format|
  22
+      format.html # index.html.erb
  23
+      format.xml  { render :xml => @annotations }
  24
+    end
  25
+  end
  26
+
  27
+  # GET /annotations/1
  28
+  # GET /annotations/1.xml
  29
+  def show
  30
+    respond_to do |format|
  31
+      format.html # show.html.erb
  32
+      format.xml  { render :xml => @annotation }
  33
+    end
  34
+  end
  35
+
  36
+  # GET /annotations/new
  37
+  # GET /annotations/new.xml
  38
+  def new
  39
+    @annotation = Annotation.new
  40
+
  41
+    respond_to do |format|
  42
+      format.html # new.html.erb
  43
+      format.xml  { render :xml => @annotation }
  44
+    end
  45
+  end
  46
+
  47
+  # POST /annotations
  48
+  # POST /annotations.xml
  49
+  def create
  50
+    if params[:annotation][:source_type].blank? and params[:annotation][:source_id].blank?
  51
+      if logged_in?
  52
+        params[:annotation][:source_type] = current_user.class.name
  53
+        params[:annotation][:source_id] = current_user.id
  54
+      end
  55
+    end
  56
+    
  57
+    @annotation = Annotation.new(params[:annotation])
  58
+    @annotation.annotatable = @annotatable
  59
+
  60
+    respond_to do |format|
  61
+      if @annotation.save
  62
+        flash[:notice] = 'Annotation was successfully created.'
  63
+        format.html { redirect_to :back }
  64
+        format.xml  { render :xml => @annotation, :status => :created, :location => @annotation }
  65
+      else
  66
+        format.html { render :action => "new" }
  67
+        format.xml  { render :xml => @annotation.errors, :status => :unprocessable_entity }
  68
+      end
  69
+    end
  70
+  end
  71
+  
  72
+  # POST /annotations/create_multiple
  73
+  # POST /annotations/create_multiple.xml
  74
+  def create_multiple
  75
+    if params[:annotation][:source_type].blank? and params[:annotation][:source_id].blank?
  76
+      if logged_in?
  77
+        params[:annotation][:source_type] = current_user.class.name
  78
+        params[:annotation][:source_id] = current_user.id
  79
+      end
  80
+    end
  81
+    
  82
+    success, annotations, errors = Annotation.create_multiple(params[:annotation], params[:separator])
  83
+
  84
+    respond_to do |format|
  85
+      if success
  86
+        flash[:notice] = 'Annotations were successfully created.'
  87
+        format.html { redirect_to :back }
  88
+        format.xml  { render :xml => annotations, :status => :created, :location => @annotatable }
  89
+      else
  90
+        flash[:error] = 'Some or all annotations failed to be created.'
  91
+        format.html { redirect_to :back }
  92
+        format.xml  { render :xml => annotations + errors, :status => :unprocessable_entity }
  93
+      end
  94
+    end
  95
+  end
  96
+  
  97
+  # GET /annotations/1/edit
  98
+  def edit
  99
+  end
  100
+
  101
+  # PUT /annotations/1
  102
+  # PUT /annotations/1.xml
  103
+  def update
  104
+    @annotation.value = params[:annotation][:value]
  105
+    @annotation.version_creator_id = current_user.id
  106
+    respond_to do |format|
  107
+      if @annotation.save
  108
+        flash[:notice] = 'Annotation was successfully updated.'
  109
+        format.html { redirect_to :back }
  110
+        format.xml  { head :ok }
  111
+      else
  112
+        format.html { render :action => "edit" }
  113
+        format.xml  { render :xml => @annotation.errors, :status => :unprocessable_entity }
  114
+      end
  115
+    end
  116
+  end
  117
+
  118
+  # DELETE /annotations/1
  119
+  # DELETE /annotations/1.xml
  120
+  def destroy
  121
+    @annotation.destroy
  122
+
  123
+    respond_to do |format|
  124
+      flash[:notice] = 'Annotation successfully deleted.'
  125
+      format.html { redirect_to :back }
  126
+      format.xml  { head :ok }
  127
+    end
  128
+  end
  129
+  
  130
+  protected
  131
+  
  132
+  def find_annotation
  133
+    @annotation = Annotation.find(params[:id])
  134
+  end
  135
+  
  136
+  def find_annotatable
  137
+    @annotatable = nil
  138
+    
  139
+    if params[:annotation]
  140
+      @annotatable = Annotation.find_annotatable(params[:annotation][:annotatable_type], params[:annotation][:annotatable_id])
  141
+    end
  142
+    
  143
+    # If still nil try again with alternative params
  144
+    if @annotatable.nil?
  145
+      @annotatable = Annotation.find_annotatable(params[:annotatable_type], params[:annotatable_id])
  146
+    end
  147
+  end
  148
+  
  149
+  # Currently only checks that the source of the annotation matches the current user
  150
+  def authorise_action
  151
+    if !logged_in? or (@annotation.source != current_user)
  152
+      # TODO: return either a 401 or 403 depending on authentication
  153
+      respond_to do |format|
  154
+        flash[:error] = 'You are not allowed to perform this action.'
  155
+        format.html { redirect_to :back }
  156
+        format.xml  { head :forbidden }
  157
+      end
  158
+      return false
  159
+    end
  160
+    return true
  161
+  end
  162
+end
2  lib/app/controllers/application_controller.rb
... ...
@@ -0,0 +1,2 @@
  1
+class ApplicationController < ActionController::Base
  2
+end
2  lib/app/helpers/application_helper.rb
... ...
@@ -0,0 +1,2 @@
  1
+module ApplicationHelper
  2
+end
329  lib/app/models/annotation.rb
... ...
@@ -0,0 +1,329 @@
  1
+class Annotation < ActiveRecord::Base
  2
+  include AnnotationsVersionFu
  3
+  
  4
+  before_validation_on_create :set_default_value_type
  5
+  
  6
+  before_validation :process_value_adjustments
  7
+  
  8
+  belongs_to :annotatable, 
  9
+             :polymorphic => true
  10
+  
  11
+  belongs_to :source, 
  12
+             :polymorphic => 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
+  validates_presence_of :source_type,
  22
+                        :source_id,
  23
+                        :annotatable_type,
  24
+                        :annotatable_id,
  25
+                        :attribute_id,
  26
+                        :value,
  27
+                        :value_type
  28
+                        
  29
+  validate :check_annotatable,
  30
+           :check_source,
  31
+           :check_duplicate,
  32
+           :check_limit_per_source,
  33
+           :check_value_restrictions
  34
+           
  35
+  # ========================
  36
+  # Versioning configuration
  37
+  # ------------------------
  38
+  
  39
+  annotations_version_fu do
  40
+    belongs_to :annotatable, 
  41
+               :polymorphic => true
  42
+    
  43
+    belongs_to :source, 
  44
+               :polymorphic => true
  45
+               
  46
+    belongs_to :attribute,
  47
+               :class_name => "AnnotationAttribute",
  48
+               :foreign_key => "attribute_id"
  49
+             
  50
+    belongs_to :version_creator, 
  51
+               :class_name => "::#{Annotations::Config.user_model_name}"
  52
+    
  53
+    validates_presence_of :source_type,
  54
+                          :source_id,
  55
+                          :annotatable_type,
  56
+                          :annotatable_id,
  57
+                          :attribute_id,
  58
+                          :value,
  59
+                          :value_type
  60
+  end
  61
+  
  62
+  # ========================
  63
+  
  64
+  # Returns all the annotatable objects that have a specified attribute name and value.
  65
+  #
  66
+  # NOTE (1): both the attribute name and the value will be treated case insensitively.
  67
+  def self.find_annotatables_with_attribute_name_and_value(attribute_name, value)
  68
+    return [ ] if attribute_name.blank? or value.nil?
  69
+    
  70
+    anns = Annotation.find(:all,
  71
+                           :joins => :attribute,
  72
+                           :conditions => { :annotation_attributes =>  { :name => attribute_name.strip.downcase }, 
  73
+                                            :value => value.strip.downcase })
  74
+                                                  
  75
+    return anns.map{|a| a.annotatable}
  76
+  end
  77
+  
  78
+  # Same as the Annotation.find_annotatables_with_attribute_name_and_value method but 
  79
+  # takes in arrays for attribute names and values.
  80
+  #
  81
+  # This allows you to build any combination of attribute names and values to search on.
  82
+  # E.g. (1): Annotation.find_annotatables_with_attribute_names_and_values([ "tag" ], [ "fiction", "sci-fi", "fantasy" ])
  83
+  # E.g. (2): Annotation.find_annotatables_with_attribute_names_and_values([ "tag", "keyword", "category" ], [ "fiction", "fantasy" ])
  84
+  #
  85
+  # NOTE (1): the arguments to this method MUST be Arrays of Strings.
  86
+  # NOTE (2): all attribute names and the values will be treated case insensitively.
  87
+  def self.find_annotatables_with_attribute_names_and_values(attribute_names, values)
  88
+    return [ ] if attribute_names.blank? or values.blank?
  89
+    
  90
+    anns = Annotation.find(:all,
  91
+                           :joins => :attribute,
  92
+                           :conditions => { :annotation_attributes =>  { :name => attribute_names }, 
  93
+                                            :value => values })
  94
+    
  95
+    return anns.map{|a| a.annotatable}
  96
+  end
  97
+  
  98
+  # Finder to get all annotations by a given source.
  99
+  named_scope :by_source, lambda { |source_type, source_id| 
  100
+    { :conditions => { :source_type => source_type, 
  101
+                       :source_id => source_id },
  102
+      :order => "created_at DESC" }
  103
+  }
  104
+  
  105
+  # Finder to get all annotations for a given annotatable.
  106
+  named_scope :for_annotatable, lambda { |annotatable_type, annotatable_id| 
  107
+    { :conditions => { :annotatable_type =>  annotatable_type, 
  108
+                       :annotatable_id => annotatable_id },
  109
+      :order => "created_at DESC" }
  110
+  }
  111
+  
  112
+  # Finder to get all annotations with a given attribute_name.
  113
+  named_scope :with_attribute_name, lambda { |attrib_name|
  114
+    { :conditions => { :annotation_attributes => { :name => attrib_name } },
  115
+      :joins => :attribute,
  116
+      :order => "created_at DESC" }
  117
+  }
  118
+  
  119
+  # Helper class method to look up an annotatable object
  120
+  # given the annotatable class name and ID. 
  121
+  def self.find_annotatable(annotatable_type, annotatable_id)
  122
+    return nil if annotatable_type.nil? or annotatable_id.nil?
  123
+    begin
  124
+      return annotatable_type.constantize.find(annotatable_id)
  125
+    rescue
  126
+      return nil
  127
+    end
  128
+  end
  129
+  
  130
+  # Helper class method to look up a source object
  131
+  # given the source class name and ID. 
  132
+  def self.find_source(source_type, source_id)
  133
+    return nil if source_type.nil? or source_id.nil?
  134
+    begin
  135
+      return source_type.constantize.find(source_id)
  136
+    rescue
  137
+      return nil
  138
+    end
  139
+  end
  140
+  
  141
+  def attribute_name
  142
+    self.attribute.name
  143
+  end
  144
+  
  145
+  def attribute_name=(attr_name)
  146
+    attr_name = attr_name.to_s.strip
  147
+    self.attribute = AnnotationAttribute.find_or_create_by_name(attr_name)
  148
+  end
  149
+  
  150
+  def value=(value_in)
  151
+    self[:value] = value_in.to_s
  152
+  end
  153
+  
  154
+  def self.create_multiple(params, separator)
  155
+    success = true
  156
+    annotations = [ ]
  157
+    errors = [ ]
  158
+    
  159
+    annotatable = Annotation.find_annotatable(params[:annotatable_type], params[:annotatable_id])
  160
+    
  161
+    if annotatable
  162
+      values = params[:value]
  163
+      
  164
+      # Remove value from params hash
  165
+      params.delete("value")
  166
+      
  167
+      values.split(separator).each do |val|
  168
+        ann = Annotation.new(params)
  169
+        ann.value = val.strip
  170
+        
  171
+        if ann.save
  172
+          annotations << ann
  173
+        else
  174
+          error_text = "Error(s) occurred whilst saving annotation with attribute: '#{params[:attribute_name]}', and value: #{val} - #{ann.errors.full_messages.to_sentence}." 
  175
+          errors << error_text
  176
+          logger.info(error_text)
  177
+          success = false
  178
+        end
  179
+      end
  180
+    else
  181
+      errors << "Annotatable object doesn't exist"
  182
+      success = false
  183
+    end
  184
+     
  185
+    return [ success, annotations, errors ]
  186
+  end
  187
+  
  188
+  protected
  189
+  
  190
+  def set_default_value_type
  191
+    self.value_type = "String" if self.value_type.blank?
  192
+  end
  193
+  
  194
+  def process_value_adjustments
  195
+    attr_name = self.attribute_name.downcase
  196
+    # Make lowercase or uppercase if required
  197
+    self.value.downcase! if Annotations::Config::attribute_names_for_values_to_be_downcased.include?(attr_name)
  198
+    self.value.upcase! if Annotations::Config::attribute_names_for_values_to_be_upcased.include?(attr_name)
  199
+    
  200
+    # Apply strip text rules
  201
+    Annotations::Config::strip_text_rules.each do |attr, strip_rules|
  202
+      if attr_name == attr.downcase
  203
+        if strip_rules.is_a? Array
  204
+          strip_rules.each do |s|
  205
+            self.value = self.value.gsub(s, '')
  206
+          end
  207
+        elsif strip_rules.is_a? String or strip_rules.is_a? Regexp
  208
+          self.value = self.value.gsub(strip_rules, '')
  209
+        end
  210
+      end
  211
+    end
  212
+  end
  213
+  
  214
+  # ===========
  215
+  # Validations
  216
+  # -----------
  217
+  
  218
+  def check_annotatable
  219
+    if Annotation.find_annotatable(self.annotatable_type, self.annotatable_id).nil?
  220
+      self.errors.add(:annotatable_id, "doesn't exist")
  221
+      return false
  222
+    else
  223
+      return true
  224
+    end
  225
+  end
  226
+  
  227
+  def check_source
  228
+    if Annotation.find_source(self.source_type, self.source_id).nil?
  229
+      self.errors.add(:source_id, "doesn't exist")
  230
+      return false
  231
+    else
  232
+      return true
  233
+    end
  234
+  end
  235
+  
  236
+  # This method checks whether duplicates are allowed for this particular annotation type (ie: 
  237
+  # for the attribute that this annotation belongs to). If not, it checks for a duplicate existing annotation.
  238
+  def check_duplicate
  239
+    attr_name = self.attribute_name.downcase
  240
+    if Annotations::Config.attribute_names_to_allow_duplicates.include?(attr_name)
  241
+      return true
  242
+    else
  243
+      existing = Annotation.find(:all,
  244
+                                 :joins => [ :attribute ],
  245
+                                 :conditions => { :annotatable_type =>  self.annotatable_type, 
  246
+                                                  :annotatable_id => self.annotatable_id, 
  247
+                                                  :value => self.value,
  248
+                                                  :annotation_attributes => { :name => attr_name  } })
  249
+      
  250
+      if existing.length == 0 or existing[0].id == self.id
  251
+        # It's all good...
  252
+        return true
  253
+      else
  254
+        self.errors.add_to_base("This annotation already exists and is not allowed to be created again.")
  255
+        return false
  256
+      end
  257
+    end
  258
+  end
  259
+