my_annotations 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|