congo 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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