congo 0.1.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.
@@ -0,0 +1,24 @@
1
+ module Congo
2
+ module Metadata
3
+ class Association
4
+ include MongoMapper::EmbeddedDocument
5
+
6
+ key :name, String
7
+ key :type, String, :default => 'many'
8
+
9
+ validates_presence_of :name, :type,
10
+ :message => lambda { I18n.t('congo.errors.messages.empty') }
11
+
12
+ validates_inclusion_of :type,
13
+ :within => ['many', 'belongs_to'],
14
+ :message => lambda { I18n.t('congo.errors.messages.inclusion') }
15
+
16
+ def apply(klass, scope)
17
+ klass.send(type, name, :class => scope.content_type_as_const(name.classify))
18
+ if type.to_sym == :belongs_to
19
+ klass.key name.foreign_key, String
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ module Congo
2
+ module Metadata
3
+ class Key
4
+ include MongoMapper::EmbeddedDocument
5
+
6
+ ## keys
7
+ key :name, String
8
+ key :label, String
9
+ key :type, String, :default => 'String'
10
+
11
+ ## validations
12
+ validates_presence_of :name, :type,
13
+ :message => lambda { I18n.t('congo.errors.messages.empty') }
14
+
15
+ ## callbacks
16
+ before_validation { |c| c.send(:normalize_name) }
17
+
18
+ def apply(klass, scope)
19
+ if type == 'File'
20
+ klass.send(:include, Congo::Grip::HasAttachment) unless klass.include?(Congo::Grip::HasAttachment) # do not add the module twice
21
+ klass.has_grid_attachment name.to_sym, :path => ":name/:id"
22
+ else
23
+ ctype = scope.content_type_as_const(type)
24
+ klass.key name.to_sym, ctype
25
+ klass.include_errors_from name.to_sym if ctype.instance_methods.include?('valid?')
26
+ ctype.apply(klass, scope, name) if ctype.respond_to?('apply')
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_name
33
+ # TODO: find something more robust to convert label to name
34
+ self.name = self.label.underscore.gsub(' ', '_') if self.name.blank? && self.label
35
+ self.name.downcase! if self.name
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ module Congo
2
+ module Metadata
3
+ class Validation
4
+ include MongoMapper::EmbeddedDocument
5
+
6
+ key :type, String
7
+ key :key, String
8
+ key :argument, String
9
+
10
+ validates_presence_of :key,
11
+ :message => lambda { I18n.t('congo.errors.messages.empty') }
12
+
13
+ validates_inclusion_of :type,
14
+ :within => methods.grep(/^validates_/).map { |m| m.gsub(/^validates_/, '') },
15
+ :message => lambda { I18n.t('congo.errors.messages.inclusion') }
16
+
17
+ def apply(klass, scope)
18
+ options = { :message => type_to_i18n }
19
+ case self.type
20
+ when 'format_of'
21
+ options.merge! :with => /#{self.argument}/
22
+ end
23
+ klass.send("validates_#{type}", key, options)
24
+ end
25
+
26
+ protected
27
+
28
+ def type_to_i18n
29
+ keyword = (case self.type
30
+ when 'presence_of' then 'empty'
31
+ when 'acceptance_of' then 'accepted'
32
+ when 'associated' then 'invalid'
33
+ when 'format_of' then 'invalid'
34
+ when 'length_of' then 'wrong_length'
35
+ when 'numericality_of' then 'not_a_number'
36
+ else self.type.gsub('_of', '')
37
+ end)
38
+ I18n.t("congo.errors.messages.#{keyword}")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,143 @@
1
+ module Congo
2
+ module Migration
3
+
4
+ def self.included(model)
5
+ model.class_eval do
6
+ include InstanceMethods
7
+
8
+ key :version, Integer, :default => 0
9
+
10
+ many :migrations, :class_name => 'Congo::Migration::Migration', :dependent => :destroy
11
+
12
+ before_save :increment_version_and_build_migration
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+
18
+ private
19
+
20
+ def apply_migration(klass)
21
+ klass.key :_version, Integer, :default => version
22
+
23
+ klass.class_eval <<-EOV
24
+ def initialize_with_version(attrs={}, from_database = false)
25
+ initialize_without_version(attrs, from_database)
26
+
27
+ self.migrate!
28
+ end
29
+
30
+ alias_method_chain :initialize, :version
31
+
32
+ def out_dated?
33
+ self._version < content_type.version
34
+ end
35
+
36
+ def migrate!
37
+ content_type.send(:migrate!, self)
38
+ end
39
+
40
+ protected
41
+
42
+ def rename_key(old_key_name, new_key_name)
43
+ @_mongo_doc ||= self.class.collection.find({ '_id' => self._id }).first
44
+ @_mongo_doc[new_key_name] = @_mongo_doc[old_key_name]
45
+
46
+ @_mongo_doc.delete(old_key_name)
47
+ end
48
+
49
+ def drop_key(key_name)
50
+ @_mongo_doc ||= self.class.collection.find({ '_id' => self._id }).first
51
+ @_mongo_doc.delete(key_name)
52
+ self.send(:keys).delete(key_name) rescue nil
53
+ end
54
+ EOV
55
+ end
56
+
57
+ def migrate!(content)
58
+ return false unless content.out_dated?
59
+
60
+ doc = content.class.collection.find({ '_id' => content._id }).first
61
+ content.instance_variable_set '@_mongo_doc', doc
62
+
63
+ migrations.each do |migration|
64
+ if doc['_version'] < migration.version
65
+ # logger.debug "running migration #{migration.version} / #{migration.inspect}"
66
+
67
+ migration.tasks.each do |task|
68
+ # logger.debug "...running task #{task['action']}"
69
+
70
+ case task['action'].to_sym
71
+ when :rename
72
+ content.send(:rename_key, task['previous'], task['next'])
73
+ when :drop
74
+ content.send(:drop_key, task['previous'])
75
+ else
76
+ # unknown action
77
+ end
78
+ end
79
+ doc['_version'] = migration.version
80
+ # logger.debug "finishing migration (#{content.version}) / #{doc.inspect}"
81
+ # puts "finishing migration (#{migration.version}) / #{doc.inspect}"
82
+ end
83
+ end
84
+ content.class.collection.save(doc)
85
+
86
+ content.attributes = doc
87
+ end
88
+
89
+ def increment_version_and_build_migration
90
+ return if self.to_const.count == 0
91
+
92
+ current_ids = metadata_keys.collect { |k| k['_id'] }.compact
93
+ previous_ids = metadata_keys.previous.collect { |k| k['_id'] }.compact
94
+
95
+ migration = Migration.new(:version => self.version + 1, :tasks => [])
96
+
97
+ # renamed keys ?
98
+ (previous_ids & current_ids).each do |key_id|
99
+ current, previous = metadata_keys.find(key_id), metadata_keys.previous.detect { |k| k['_id'] == key_id }
100
+ if previous['name'] != current['name']
101
+ migration.tasks << { :action => 'rename', :previous => previous['name'], :next => current['name'] }
102
+
103
+ # check for potential validations relative to the modified key
104
+ metadata_validations.each do |validation|
105
+ validation.key = current['name'] if validation.key == previous['name']
106
+ end
107
+ end
108
+ end
109
+
110
+ # dropped keys ?
111
+ (previous_ids - current_ids).each do |key_id|
112
+ previous = metadata_keys.previous.detect { |k| k['_id'] == key_id }
113
+ migration.tasks << { :action => 'drop', :previous => previous['name'] }
114
+
115
+ # check for potential validations relative to the dropped key
116
+ metadata_validations.delete_if { |v| v.key == previous['name'] }
117
+ end
118
+
119
+ unless migration.empty?
120
+ self.version += 1
121
+ self.migrations << migration
122
+ # logger.debug "incrementing version #{self.version}"
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ class Migration
129
+ include MongoMapper::EmbeddedDocument
130
+
131
+ ## keys
132
+ key :version, Integer, :required => true
133
+ key :tasks, Array, :required => true
134
+
135
+ ## methods
136
+
137
+ def empty?
138
+ self.tasks.empty?
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,18 @@
1
+ module Congo
2
+ class ProxyScoper
3
+
4
+ include MongoMapper::Document
5
+
6
+ acts_as_congo_scoper
7
+
8
+ ## keys
9
+ key :ext_type, String
10
+ key :ext_id, Integer
11
+
12
+ ## validations
13
+ validates_true_for :ext,
14
+ :logic => lambda { Congo::ProxyScoper.find_by_ext_id_and_ext_type(ext_id, ext_type).nil? },
15
+ :message => lambda { I18n.t('congo.errors.messages.taken') }
16
+
17
+ end
18
+ end
@@ -0,0 +1,81 @@
1
+ module Congo
2
+ module Scoper #:nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def acts_as_congo_scoper
9
+ if !self.included_modules.include?(MongoMapper::Document)
10
+ class_eval <<-EOV
11
+
12
+ include Congo::Scoper::InstanceMethods
13
+
14
+ def scoper_instance
15
+ @proxy_scoper ||= Congo::ProxyScoper.find_by_ext_id_and_ext_type(self.id, self.class.name)
16
+ if @proxy_scoper.nil?
17
+ @proxy_scoper = Congo::ProxyScoper.create!(:ext_type => self.class.name, :ext_id => self.id)
18
+ end
19
+ @proxy_scoper
20
+ end
21
+
22
+ def content_types
23
+ scoper_instance.content_types
24
+ end
25
+ EOV
26
+ else
27
+ class_eval <<-EOV
28
+
29
+ include Congo::Scoper::InstanceMethods
30
+
31
+ many :content_types, :class_name => 'Congo::ContentType', :as => :scope
32
+
33
+ def scoper_instance
34
+ nil
35
+ end
36
+ EOV
37
+ end
38
+ end
39
+ end
40
+
41
+ module InstanceMethods
42
+
43
+ def proxy_scoper?
44
+ !scoper_instance.nil?
45
+ end
46
+
47
+ def consts
48
+ @consts ||= {}
49
+ end
50
+
51
+ def content_type_as_const(name, method_name = nil)
52
+ return nil if (Congo::Types.const_defined?(name) rescue nil).nil?
53
+ return Congo::Types.const_get(name) if Congo::Types.const_defined?(name)
54
+ return name.constantize if Object.const_defined?(name)
55
+
56
+ unless consts[name]
57
+ if (type = content_types.find_by_name(name)).nil?
58
+ type = content_types.find_by_slug(method_name) if method_name
59
+ end
60
+ consts[name] = type.to_const rescue nil # This doesn't work because of different instances being used
61
+ end
62
+
63
+ consts[name]
64
+ end
65
+
66
+ private
67
+
68
+ def method_missing(method, *args)
69
+ if ctype = content_type_as_const(method.to_s.classify, method.to_s)
70
+ meta = proxy_scoper? ? scoper_instance.metaclass : metaclass
71
+ meta.many method, :class => ctype
72
+ (proxy_scoper? ? scoper_instance : self).send(method, *args)
73
+ else
74
+ super
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ Object.class_eval { include Congo::Scoper }
@@ -0,0 +1,19 @@
1
+ module MongoMapper
2
+ module Plugins
3
+ module Associations
4
+ class ManyEmbeddedProxy < EmbeddedCollection
5
+
6
+ def replace_with_dirty_mode(values)
7
+ @_previous_values = @_values
8
+ replace_without_dirty_mode(values)
9
+ end
10
+
11
+ def previous
12
+ @_previous_values || []
13
+ end
14
+
15
+ alias_method_chain :replace, :dirty_mode
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ module Congo
2
+ module Types
3
+
4
+ class Email < String
5
+
6
+ def self.apply(klass, scope, name)
7
+ klass.class_eval do
8
+ validates_format_of name.to_sym,
9
+ :with => /\A[\w\._%-]+@[\w\.-]+\.[a-zA-Z]{2,4}\z/,
10
+ :message => lambda { I18n.t('congo.errors.messages.invalid') }
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ class Text < String
17
+ end
18
+
19
+ class Date < Date
20
+
21
+ def self.apply(klass, scope, name)
22
+ klass.class_eval <<-EOV
23
+
24
+ def localized_#{name}
25
+ if self.#{name}
26
+ self.#{name}.strftime(I18n.t('congo.date.formats.default'))
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ def localized_#{name}=(value)
33
+ self.#{name} = value
34
+ end
35
+
36
+ EOV
37
+ end
38
+
39
+ def self.to_mongo(value)
40
+ date = self.parse_with_i18n(value.to_s)
41
+ Time.utc(date[:year], date[:mon], date[:mday])
42
+ end
43
+
44
+ def self.from_mongo(value)
45
+ value.to_date if value.present?
46
+ end
47
+
48
+ # Patch from http://gist.github.com/179712
49
+ def self.parse_with_i18n(str)
50
+ date = ::Date._strptime(str, I18n.t('congo.date.formats.default')) || self._parse(str)
51
+ date[:year] += self.increment_year(date[:year].to_i) if date[:year]
52
+ date
53
+ end
54
+
55
+ def self.increment_year(year)
56
+ if year < 100
57
+ year < 30 ? 2000 : 1900
58
+ else
59
+ 0
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,86 @@
1
+ module Congo
2
+ module Validation
3
+
4
+ def self.included(model)
5
+ model.class_eval do
6
+ include InstanceMethods
7
+
8
+ validates_format_of :name,
9
+ :with => /^[a-zA-Z][\w\s]*$/,
10
+ :message => lambda { I18n.t('congo.errors.messages.invalid') }
11
+
12
+ validates_presence_of :name, :scope,
13
+ :message => lambda { I18n.t('congo.errors.messages.empty') }
14
+
15
+ validates_true_for :name,
16
+ :logic => :check_uniqueness_of_name,
17
+ :message => lambda { I18n.t('congo.errors.messages.taken') }
18
+
19
+ validates_true_for :name,
20
+ :key => :allowed_name,
21
+ :logic => :check_allowed_name,
22
+ :message => lambda { I18n.t('congo.errors.messages.exclusion') }
23
+
24
+ validates_true_for :metadata_keys,
25
+ :logic => :validate_metadata_keys,
26
+ :message => lambda { I18n.t('congo.errors.messages.invalid') }
27
+
28
+ validates_true_for :collection_name,
29
+ :logic => lambda { name.present? },
30
+ :message => lambda { I18n.t('congo.errors.messages.empty') }
31
+
32
+ validates_true_for :collection_name,
33
+ :key => :unique_collection_name,
34
+ :logic => :check_uniqueness_of_collection_name,
35
+ :message => lambda { I18n.t('congo.errors.messages.taken') }
36
+
37
+ validates_true_for :collection_name,
38
+ :key => :allowed_allowed_name,
39
+ :logic => :check_allowed_name,
40
+ :message => lambda { I18n.t('congo.errors.messages.exclusion') }
41
+ end
42
+ end
43
+
44
+ module InstanceMethods
45
+
46
+ private
47
+
48
+ def validate_metadata_keys
49
+ return false if metadata_keys.empty?
50
+
51
+ # keys should be valid and unique
52
+ found_errors, duplicates = false, {}
53
+ metadata_keys.each do |key|
54
+ found_errors ||= !key.valid?
55
+ if duplicates.key?(key.name)
56
+ key.errors.add(:name, I18n.t('activerecord.errors.messages.taken'))
57
+ found_errors = true
58
+ else
59
+ duplicates[key.name] = key
60
+ end
61
+ end
62
+ !found_errors
63
+ end
64
+
65
+ def check_uniqueness_of(attribute, value)
66
+ type = self.class.first(:conditions => { :scope_type => scope_type, :scope_id => scope_id, attribute => value })
67
+ type.nil? || (type && type == self)
68
+ end
69
+
70
+ def check_uniqueness_of_name
71
+ check_uniqueness_of(:name, name)
72
+ end
73
+
74
+ def check_uniqueness_of_collection_name
75
+ collection_name.blank? || (collection_name.present? && check_uniqueness_of(:slug, slug))
76
+ end
77
+
78
+ def check_allowed_name
79
+ return true if name.blank? || scope.nil?
80
+ methods = scope.is_a?(Congo::ProxyScoper) ? scope.ext_type.constantize.instance_methods : scope.methods
81
+ !(methods.include?(name.tableize.to_s) || (slug.present? && methods.include?(slug)))
82
+ end
83
+
84
+ end
85
+ end
86
+ end