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.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/AUTHORS.rdoc +5 -0
- data/CHANGELOG.rdoc +64 -0
- data/INDEX.rdoc +17 -0
- data/generators/annotations_migration/annotations_migration_generator.rb +32 -0
- data/generators/annotations_migration/templates/migration_v1.rb +60 -0
- data/generators/annotations_migration/templates/migration_v2.rb +9 -0
- data/generators/annotations_migration/templates/migration_v3.rb +74 -0
- data/generators/annotations_migration/templates/migration_v4.rb +13 -0
- data/install.rb +1 -0
- data/lib/annotations/acts_as_annotatable.rb +271 -0
- data/lib/annotations/acts_as_annotation_source.rb +117 -0
- data/lib/annotations/acts_as_annotation_value.rb +115 -0
- data/lib/annotations/config.rb +148 -0
- data/lib/annotations/routing.rb +8 -0
- data/lib/annotations/util.rb +82 -0
- data/lib/annotations_version_fu.rb +119 -0
- data/lib/app/controllers/annotations_controller.rb +162 -0
- data/lib/app/controllers/application_controller.rb +2 -0
- data/lib/app/helpers/application_helper.rb +2 -0
- data/lib/app/models/annotation.rb +413 -0
- data/lib/app/models/annotation_attribute.rb +37 -0
- data/lib/app/models/annotation_value_seed.rb +48 -0
- data/lib/app/models/number_value.rb +23 -0
- data/lib/app/models/text_value.rb +23 -0
- data/my_annotations.gemspec +4 -9
- data/rails/init.rb +8 -0
- data/test/acts_as_annotatable_test.rb +186 -0
- data/test/acts_as_annotation_source_test.rb +84 -0
- data/test/acts_as_annotation_value_test.rb +17 -0
- data/test/annotation_attribute_test.rb +22 -0
- data/test/annotation_test.rb +213 -0
- data/test/annotation_value_seed_test.rb +14 -0
- data/test/annotation_version_test.rb +39 -0
- data/test/annotations_controller_test.rb +27 -0
- data/test/app_root/app/controllers/application_controller.rb +9 -0
- data/test/app_root/app/models/book.rb +5 -0
- data/test/app_root/app/models/chapter.rb +5 -0
- data/test/app_root/app/models/group.rb +3 -0
- data/test/app_root/app/models/tag.rb +6 -0
- data/test/app_root/app/models/user.rb +3 -0
- data/test/app_root/app/views/annotations/edit.html.erb +12 -0
- data/test/app_root/app/views/annotations/index.html.erb +1 -0
- data/test/app_root/app/views/annotations/new.html.erb +11 -0
- data/test/app_root/app/views/annotations/show.html.erb +3 -0
- data/test/app_root/config/boot.rb +115 -0
- data/test/app_root/config/environment.rb +16 -0
- data/test/app_root/config/environments/mysql.rb +0 -0
- data/test/app_root/config/routes.rb +4 -0
- data/test/app_root/db/migrate/001_create_test_models.rb +38 -0
- data/test/app_root/db/migrate/002_annotations_migration_v1.rb +60 -0
- data/test/app_root/db/migrate/003_annotations_migration_v2.rb +9 -0
- data/test/app_root/db/migrate/004_annotations_migration_v3.rb +72 -0
- data/test/config_test.rb +383 -0
- data/test/fixtures/annotation_attributes.yml +49 -0
- data/test/fixtures/annotation_value_seeds.csv +16 -0
- data/test/fixtures/annotation_versions.yml +259 -0
- data/test/fixtures/annotations.yml +239 -0
- data/test/fixtures/books.yml +13 -0
- data/test/fixtures/chapters.yml +27 -0
- data/test/fixtures/groups.yml +7 -0
- data/test/fixtures/number_value_versions.csv +2 -0
- data/test/fixtures/number_values.csv +2 -0
- data/test/fixtures/text_value_versions.csv +35 -0
- data/test/fixtures/text_values.csv +35 -0
- data/test/fixtures/users.yml +8 -0
- data/test/number_value_version_test.rb +40 -0
- data/test/routing_test.rb +27 -0
- data/test/test_helper.rb +41 -0
- data/test/text_value_version_test.rb +40 -0
- metadata +77 -7
@@ -0,0 +1,115 @@
|
|
1
|
+
# ActsAsAnnotationValue
|
2
|
+
module Annotations
|
3
|
+
module Acts #:nodoc:
|
4
|
+
module AnnotationValue #:nodoc:
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def acts_as_annotation_value(options)
|
12
|
+
cattr_accessor :ann_value_content_field, :is_annotation_value
|
13
|
+
|
14
|
+
if options[:content_field].blank?
|
15
|
+
raise ArgumentError.new("Must specify the :content_field option that will be used as the field for the content")
|
16
|
+
end
|
17
|
+
|
18
|
+
self.ann_value_content_field = options[:content_field]
|
19
|
+
|
20
|
+
has_many :annotations,
|
21
|
+
:as => :value
|
22
|
+
|
23
|
+
has_many :annotation_value_seeds,
|
24
|
+
:as => :value
|
25
|
+
|
26
|
+
__send__ :extend, SingletonMethods
|
27
|
+
__send__ :include, InstanceMethods
|
28
|
+
|
29
|
+
self.is_annotation_value = true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Class methods added to the model that has been made acts_as_annotation_value (the mixin target class).
|
34
|
+
module SingletonMethods
|
35
|
+
|
36
|
+
# This class level method is used to determine whether there is an existing
|
37
|
+
# annotation on an annotatable object, regardless of source. So, it is used to
|
38
|
+
# determine whether a "duplicate" exists (but the notion of "duplicate"
|
39
|
+
# may vary between annotation value classes).
|
40
|
+
#
|
41
|
+
# This method may be redefined in your model.
|
42
|
+
# A default implementation is provided that may suffice for most cases.
|
43
|
+
# But note that it makes certain assumptions that may not be valid for all
|
44
|
+
# kinds of annotation value models:
|
45
|
+
# - the joins for the value model use +ActiveRecord::Base::table_name+ to
|
46
|
+
# determine the name of the table to join on.
|
47
|
+
# - the field used make the comparison on the content is defined by the
|
48
|
+
# ':content_field' option passed into acts_as_annotation_value.
|
49
|
+
#
|
50
|
+
# Note: A precondition to this method is: this expects a valid
|
51
|
+
# +annotation+ object (i.e. one that contains a valid +value+
|
52
|
+
# object, valid +annotatable+ object, valid +attribute+ and so on).
|
53
|
+
def has_duplicate_annotation?(annotation)
|
54
|
+
return false unless annotation.value.is_a?(self)
|
55
|
+
|
56
|
+
val_table_name = self.table_name
|
57
|
+
|
58
|
+
existing = Annotation.find(:all,
|
59
|
+
:joins => "INNER JOIN annotation_attributes ON annotation_attributes.id = annotations.attribute_id
|
60
|
+
INNER JOIN #{val_table_name} ON annotations.value_type = '#{self.name}' AND #{val_table_name}.id = annotations.value_id",
|
61
|
+
:conditions => [ "annotations.annotatable_type = ? AND
|
62
|
+
annotations.annotatable_id = ? AND
|
63
|
+
annotation_attributes.name = ? AND
|
64
|
+
#{val_table_name}.#{self.ann_value_content_field} = ?",
|
65
|
+
annotation.annotatable_type,
|
66
|
+
annotation.annotatable_id,
|
67
|
+
annotation.attribute_name,
|
68
|
+
annotation.value.send(self.ann_value_content_field) ])
|
69
|
+
|
70
|
+
if existing.length == 0 || existing.first.id == annotation.id
|
71
|
+
return false
|
72
|
+
else
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
#A set of all values that have been used, or seeded, with one of the provided attribute names
|
79
|
+
def with_attribute_names attributes
|
80
|
+
attributes = Array(attributes)
|
81
|
+
annotations = Annotation.with_attribute_names(attributes).with_value_type(self.name).include_values.collect{|ann| ann.value}
|
82
|
+
seeds = AnnotationValueSeed.with_attribute_names(attributes).with_value_type(self.name).include_values.collect{|ann| ann.value}
|
83
|
+
(annotations | seeds).uniq
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# This module contains instance methods
|
88
|
+
module InstanceMethods
|
89
|
+
|
90
|
+
#Whether this value exists with a given attribute name
|
91
|
+
def has_attribute_name? attr
|
92
|
+
!annotations.with_attribute_name(attr).empty? || !annotation_value_seeds.with_attribute_name(attr).empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
#The total number of annotations that match one or more attribute names.
|
96
|
+
def annotation_count attributes
|
97
|
+
attributes = Array(attributes)
|
98
|
+
annotations.with_attribute_names(attributes).count
|
99
|
+
end
|
100
|
+
|
101
|
+
# The actual content of the annotation value
|
102
|
+
def ann_content
|
103
|
+
self.send(self.class.ann_value_content_field)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Set the actual content of the annotation value
|
107
|
+
def ann_content=(val)
|
108
|
+
self.send("#{self.class.ann_value_content_field}=", val)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,148 @@
|
|
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 content of the values 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
|
+
@@content_restrictions = { }
|
58
|
+
|
59
|
+
# This determines what template to use to generate the unique 'identifier' for new AnnotationAttribute objects.
|
60
|
+
#
|
61
|
+
# String interpolation will be used to place the 'name' of the annotation within the template,
|
62
|
+
# in order to generate a unique identifier (usually a URI).
|
63
|
+
#
|
64
|
+
# This uses the @@attribute_name_transform_for_identifier defined below when performing the substitution.
|
65
|
+
#
|
66
|
+
# For more info on this substitution algorithm, see AnnotationAttribute#before_validation.
|
67
|
+
@@default_attribute_identifier_template = "http://www.example.org/attribute#%s"
|
68
|
+
|
69
|
+
# Defines a Proc that will be used to transform the value of AnnotationAttribute#name when generating the
|
70
|
+
# AnnotationAttribute#identifier value. See AnnotationAttribute#before_validation for more info.
|
71
|
+
@@attribute_name_transform_for_identifier = Proc.new { |name| name.to_s }
|
72
|
+
|
73
|
+
# This stores the factory Procs that are used to generate the value objects
|
74
|
+
# for annotations, based on the attribute name.
|
75
|
+
#
|
76
|
+
# - Keys should be attribute names (as Strings, in lowercase).
|
77
|
+
# - Values should either be a Proc that takes in one argument - the raw value object, that is then used
|
78
|
+
# to output the actual value to be stored. IMPORTANT: the Procs must be exhibit consistent data behaviour.
|
79
|
+
# I.e. should be able to run them over and over again without causing data inconsistencies or harmful side effects.
|
80
|
+
#
|
81
|
+
# NOTE (1): this is run BEFORE the default value generation logic in the +Annotation+ model.
|
82
|
+
# The default value generation logic will still run after the Proc.
|
83
|
+
# NOTE (2): The attribute name(s) specified MUST all be in lowercase.
|
84
|
+
@@value_factories_for_attributes = { }
|
85
|
+
|
86
|
+
# This determines the valid value types that are allowed for certain attribute names.
|
87
|
+
#
|
88
|
+
# - Keys should be attribute names (as Strings, in lowercase).
|
89
|
+
# - Values should be an Array of Strings, or single String, of valid class names for the value object type.
|
90
|
+
#
|
91
|
+
# NOTE (1): It is possible to use the above +value_factories_for_attributes+ option to achieve
|
92
|
+
# similar behaviour. However, this config option allows you to state explicitly what types are
|
93
|
+
# allowed as value objects.
|
94
|
+
# NOTE (2): The attribute name(s) specified MUST all be in lowercase.
|
95
|
+
@@valid_value_types = { }
|
96
|
+
|
97
|
+
# This determines whether versioning is enabled.
|
98
|
+
# The default behaviour is true, in which case when a new annotation is created or updated, a copy of the new version
|
99
|
+
# is stored in Annotation::Version and linked to the annotation. Likewise versions of the annotation values are created.
|
100
|
+
# By setting to false, no versions are recorded.
|
101
|
+
@@versioning_enabled = true
|
102
|
+
|
103
|
+
def self.reset
|
104
|
+
@@attribute_names_for_values_to_be_downcased = [ ]
|
105
|
+
@@attribute_names_for_values_to_be_upcased = [ ]
|
106
|
+
@@strip_text_rules = { }
|
107
|
+
@@user_model_name = "User"
|
108
|
+
@@limits_per_source = { }
|
109
|
+
@@attribute_names_to_allow_duplicates = [ ]
|
110
|
+
@@content_restrictions = { }
|
111
|
+
@@default_attribute_identifier_template = "http://www.example.org/attribute#%s"
|
112
|
+
@@attribute_name_transform_for_identifier = Proc.new { |name| name.to_s }
|
113
|
+
@@value_factories = { }
|
114
|
+
@@valid_value_types = { }
|
115
|
+
end
|
116
|
+
|
117
|
+
reset
|
118
|
+
|
119
|
+
# This makes the variables above available externally.
|
120
|
+
# Shamelessly borrowed from the GeoKit plugin.
|
121
|
+
[ :attribute_names_for_values_to_be_downcased,
|
122
|
+
:attribute_names_for_values_to_be_upcased,
|
123
|
+
:strip_text_rules,
|
124
|
+
:user_model_name,
|
125
|
+
:limits_per_source,
|
126
|
+
:attribute_names_to_allow_duplicates,
|
127
|
+
:content_restrictions,
|
128
|
+
:default_attribute_identifier_template,
|
129
|
+
:attribute_name_transform_for_identifier,
|
130
|
+
:value_factories,
|
131
|
+
:valid_value_types,
|
132
|
+
:versioning_enabled].each do |sym|
|
133
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
134
|
+
def self.#{sym}
|
135
|
+
if defined?(#{sym.to_s.upcase})
|
136
|
+
#{sym.to_s.upcase}
|
137
|
+
else
|
138
|
+
@@#{sym}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.#{sym}=(obj)
|
143
|
+
@@#{sym} = obj
|
144
|
+
end
|
145
|
+
EOS
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Annotations #:nodoc:
|
2
|
+
def self.map_routes(map, collection={}, member={}, requirements={})
|
3
|
+
map.resources :annotations,
|
4
|
+
:collection => { :create_multiple => :post }.merge(collection),
|
5
|
+
:member => {}.merge(member),
|
6
|
+
:requirements => { }.merge(requirements)
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Annotations
|
2
|
+
module Util
|
3
|
+
|
4
|
+
# Migrate existing annotations to the v3 db schema.
|
5
|
+
#
|
6
|
+
# Currently it just copies all values over to TextValue entries
|
7
|
+
# and assigns 'value' of the annotation accordingly and fixes up
|
8
|
+
# all the versions of that annotation in a naive way.
|
9
|
+
#
|
10
|
+
# NOTE (1): If you need tdifferent migration behaviour,
|
11
|
+
# redefine this method in your app (in the same namespace).
|
12
|
+
#
|
13
|
+
# NOTE (2): if individual annotations fail to migrate,
|
14
|
+
# their IDs and error info will be outputted to the console so you
|
15
|
+
# can inspect the issue.
|
16
|
+
#
|
17
|
+
# NOTE (3): this won't migrate any AnnotationValueSeed entries.
|
18
|
+
# You will need to write another migration script/method for these.
|
19
|
+
#
|
20
|
+
# NOTE (4): this makes some big assumptions about your current set of
|
21
|
+
# annotations. Please look through to make sure the logic applies.
|
22
|
+
def self.migrate_annotations_to_v3
|
23
|
+
Annotation.record_timestamps = false
|
24
|
+
|
25
|
+
Annotation.all.each do |ann|
|
26
|
+
begin
|
27
|
+
ann.transaction do
|
28
|
+
val = TextValue.new
|
29
|
+
|
30
|
+
# Handle versions
|
31
|
+
#
|
32
|
+
# NOTE: This will take a naive approach of assuming that
|
33
|
+
# only the 'old_value' field has been changed over time,
|
34
|
+
# nothing else!
|
35
|
+
|
36
|
+
# Build up the TextValue from the versions
|
37
|
+
ann.versions.each do |version|
|
38
|
+
val.text = version.old_value
|
39
|
+
val.created_at = version.created_at unless val.created_at
|
40
|
+
val.updated_at = version.updated_at
|
41
|
+
val.save!
|
42
|
+
|
43
|
+
val_version = val.versions(true).last
|
44
|
+
val_version.created_at = version.created_at
|
45
|
+
val_version.updated_at = version.updated_at
|
46
|
+
val_version.save!
|
47
|
+
end
|
48
|
+
|
49
|
+
# Assign new TextValue to Annotation
|
50
|
+
ann.value = val
|
51
|
+
ann.save!
|
52
|
+
|
53
|
+
# Only keep second to last version,
|
54
|
+
# deleting others, and resetting version
|
55
|
+
# numbers.
|
56
|
+
ann.versions(true).each do |version|
|
57
|
+
if version == ann.versions[-2]
|
58
|
+
# The one we want to keep
|
59
|
+
version.version = 1
|
60
|
+
version.value = val
|
61
|
+
version.save!
|
62
|
+
else
|
63
|
+
# Delete!
|
64
|
+
version.destroy
|
65
|
+
end
|
66
|
+
end
|
67
|
+
ann.version = 1
|
68
|
+
ann.save! # This shouldn't result in a new version
|
69
|
+
end
|
70
|
+
rescue Exception => ex
|
71
|
+
puts "FAILED to migrate annotation with ID #{ann.id}. Error message: #{ex.message}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO: similar kind of migration for annotation value seeds
|
76
|
+
|
77
|
+
Annotation.record_timestamps = true
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,119 @@
|
|
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 if Annotations::Config.versioning_enabled
|
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
|
+
reload_versioned_columns_info
|
74
|
+
end
|
75
|
+
|
76
|
+
def reload_versioned_columns_info
|
77
|
+
self.reset_column_information
|
78
|
+
self.versioned_class.reset_column_information
|
79
|
+
if self.versioned_class.table_exists?
|
80
|
+
self.versioned_columns = versioned_class.new.attributes.keys -
|
81
|
+
[versioned_class.primary_key, versioned_foreign_key, version_column, 'created_at', 'updated_at']
|
82
|
+
else
|
83
|
+
ActiveRecord::Base.logger.warn "Version Table not found"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def versioned_class
|
88
|
+
const_get versioned_class_name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
module InstanceMethods
|
94
|
+
def find_version(number)
|
95
|
+
versions.find :first, :conditions=>{:version=>number}
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_for_new_version
|
99
|
+
instatiate_revision if create_new_version?
|
100
|
+
true # Never halt save
|
101
|
+
end
|
102
|
+
|
103
|
+
# This the method to override if you want to have more control over when to version
|
104
|
+
def create_new_version?
|
105
|
+
# Any versioned column changed?
|
106
|
+
self.class.versioned_columns.detect {|a| __send__ "#{a}_changed?"}
|
107
|
+
end
|
108
|
+
|
109
|
+
def instatiate_revision
|
110
|
+
new_version = versions.build
|
111
|
+
versioned_columns.each do |attribute|
|
112
|
+
new_version.__send__ "#{attribute}=", __send__(attribute)
|
113
|
+
end
|
114
|
+
version_number = new_record? ? 1 : version + 1
|
115
|
+
new_version.version = version_number
|
116
|
+
self.version = version_number
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
class AnnotationsController < ApplicationController
|
2
|
+
|
3
|
+
before_filter :login_required, :only => [ :new, :create, :edit, :update, :destroy, :create_multiple ]
|
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
|