og 0.25.0 → 0.26.0

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  TITLE : &title Og
4
4
  NAME : &pkg og
5
- VERSION : '0.25.0'
5
+ VERSION : '0.26.0'
6
6
  STATUS : beta
7
7
 
8
8
  AUTHOR : George Moschovitis
@@ -17,7 +17,7 @@ DESCRIPTION: >
17
17
  KirbyBase, Filesystem and more.
18
18
 
19
19
  DEPENDENCIES:
20
- - [ glue, '= 0.25.0' ]
20
+ - [ glue, '= 0.26.0' ]
21
21
 
22
22
  DISTRIBUTE: [ gem, tgz, zip ]
23
23
 
data/README CHANGED
@@ -1,4 +1,4 @@
1
- = Og 0.24.0 README
1
+ = Og 0.26.0 README
2
2
 
3
3
  Og (ObjectGraph) is a powerful and elegant object-relational mapping
4
4
  library. Og manages the lifecycle of Ruby objects and provides
@@ -1,3 +1,81 @@
1
+ == Version 0.26.0
2
+
3
+ This is the release with the most community contributions. Check
4
+ out the great new stuff. Download now!
5
+
6
+ Most notable changes:
7
+
8
+ * New CacheSweeper mixin. Using this mixin allows you to keep
9
+ the cache cleaning logic in one place. This logic is called
10
+ automagically by many default Nitro/Og methods (for example
11
+ Og insert/update, scaffolding, etc). You can fully customize
12
+ the behaviour.
13
+
14
+ class Article
15
+ include CacheSweeper
16
+
17
+ def expire_affected(action = :all)
18
+ expire_affected_output('articles/view')
19
+ ...
20
+ end
21
+ end
22
+
23
+ a = Article[1]
24
+ a.title = 'New'
25
+ a.save # => calls expire_affected.
26
+
27
+ * Searchable mixin. Include this mixin to your classes to make
28
+ them searchable by the auto administration system.
29
+
30
+ * Better validations implementation. Cleaner code, less evals,
31
+ more flexible and easier to extend.
32
+
33
+ * New scaffolding / auto administration system. The implementation
34
+ is much cleaner and easier to customize. It leverages the latest
35
+ advancements (dispatcher, sweeper, etc) and adds search support,
36
+ pager, breadcrumps and more. You can define your own controls
37
+ to handle properties and relations. Stay tuned for more stuff
38
+ in the near future.
39
+
40
+ * New Og revisable mixin. Just include this mixin in your classes
41
+ and get db backed revision support for free. Here comes an
42
+ example:
43
+
44
+ class Article
45
+ is Revisable
46
+ property :body, String, :revisable => true
47
+ property :title, String
48
+ end
49
+
50
+ Automatically generates the Revision class (and the
51
+ backend schema):
52
+
53
+ class Article::Revision
54
+
55
+ article.revisions
56
+
57
+ article.revise do |a|
58
+ a.title = 'hello'
59
+ a.body = 'world'
60
+ end
61
+
62
+ article.rollback(4)
63
+
64
+ * Bug fixed KirbyBase Og adapter. This works great with the
65
+ new 2.5 gem.
66
+
67
+ * Added more rational defaults, and many predefined options to
68
+ minimize the amount of setup needed to get your app running. Of
69
+ course you can still customize just about everything in Nitro.
70
+
71
+ * Improvements to PostgreSQL automatic generation of foreign key
72
+ constraints.
73
+
74
+ * Added evolution support to the MySql store.
75
+
76
+ * Many, many, many bug fixes and smaller improvements.
77
+
78
+
1
79
  == Version 0.25.0
2
80
 
3
81
  This is the first in a series of releases focused on stability
@@ -1,2 +1,124 @@
1
+ module Glue
1
2
 
2
- raise 'This is not working yet, do not require this file.'
3
+ # Revision support for Og-managed classes.
4
+ #
5
+ # class Article
6
+ # is Revisable
7
+ # property :body, String, :revisable => true
8
+ # property :title, String
9
+ # end
10
+ #
11
+ # Generates the Revision class:
12
+ #
13
+ # class Article::Revision
14
+ #
15
+ # article.revisions
16
+ #
17
+ # article.revise do |a|
18
+ # a.title = 'hello'
19
+ # a.body = 'world'
20
+ # end
21
+ #
22
+ # article.rollback(4)
23
+
24
+ module Revisable
25
+ # The revision of the revisable object.
26
+
27
+ property :revision, Fixnum
28
+
29
+ # This mixin is injected into the dynamically generated
30
+ # Revision class. You can customize this in your application
31
+ # to store extra fields per revision.
32
+
33
+ module Mixin
34
+ # The create time for the revision.
35
+
36
+ property :create_time, Time
37
+
38
+ # Override to handle your options.
39
+
40
+ def initialize(obj, options = {})
41
+ revision_from(obj)
42
+ @create_time = Time.now
43
+ end
44
+
45
+ def revision_from(obj)
46
+ for prop in obj.class.properties.values
47
+ # gmosx, FIXME: test against primary key, not oid.
48
+ unless prop.symbol == :oid
49
+ instance_variable_set "@#{prop}", obj.send(prop.to_s)
50
+ end
51
+ end
52
+ end
53
+
54
+ def revision_to(obj)
55
+ for prop in obj.class.properties.values
56
+ # gmosx, FIXME: test against primary key, not oid.
57
+ unless prop.symbol == :oid
58
+ obj.instance_variable_set "@#{prop}", self.send(prop.to_s)
59
+ end
60
+ end
61
+ end
62
+ alias_method :apply_to, :revision_to
63
+ end
64
+
65
+ def self.included(base)
66
+ super
67
+
68
+ base.module_eval %{
69
+ class Revision < #{base}
70
+ include Revisable::Mixin
71
+ belongs_to #{base}
72
+ end
73
+ }
74
+
75
+ base.has_many :revisions, base::Revision
76
+ end
77
+
78
+ # Can accept options like owner or a comment.
79
+ # You can specialize this in your app.
80
+
81
+ def revise(options = {})
82
+ if self.revision.nil?
83
+ self.revision = 1
84
+ else
85
+ self.revision += 1
86
+ end
87
+ self.revisions << self.class::Revision.new(self, options)
88
+ yield(self) if block_given?
89
+ self.save
90
+ end
91
+ alias_method :revise!, :revise
92
+
93
+ # Rollback to an older revision.
94
+
95
+ def rollback(rev, options = {})
96
+ self.revise(options) { |obj| get_revision(rev).apply_to(obj) }
97
+ end
98
+
99
+ # Return a revision.
100
+
101
+ def get_revision(rev)
102
+ return self if rev.to_i == self.revision
103
+ self.revisions.find_one(:condition => "revision=#{rev}")
104
+ end
105
+
106
+ # Return the last revision.
107
+
108
+ def last_revision
109
+ self.revisions(:order => 'revision DESC', :limit => 1).first
110
+ end
111
+
112
+ # The number of revisions.
113
+
114
+ def revision_count
115
+ self.revisions.count
116
+ end
117
+ alias_method :revisions_count, :revision_count
118
+
119
+ end
120
+
121
+ end
122
+
123
+ # * George Moschovitis <gm@navel.gr>
124
+ # * Dirk Barnikel <dirk.barnikel@gmx.de>
@@ -0,0 +1,28 @@
1
+ module Glue
2
+
3
+ # Search support for Og managed classes.
4
+
5
+ module Searchable
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Override this method in your class to customize the
14
+ # search. This is a nice default method.
15
+
16
+ def search(query)
17
+ search_props = properties.values.select { |p| p.searchable }
18
+ condition = search_props.collect { |p| "#{p} LIKE '%#{query}%'" }.join(' OR ')
19
+ all(:condition => condition)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ # * George Moschovitis <gm@navel.gr>
@@ -41,6 +41,10 @@ class Tag
41
41
  def self.total_frequency(tags = Tag.all)
42
42
  tags.inject(1) { |total, t| total += t.count }
43
43
  end
44
+
45
+ def to_s
46
+ @name
47
+ end
44
48
  end
45
49
 
46
50
  module Glue
@@ -65,7 +69,12 @@ module Glue
65
69
  # Tag.find_by_name('ruby').articles
66
70
 
67
71
  module Taggable
72
+ # The tag string separator.
73
+
74
+ setting :separator, :default => ' ', :doc => 'The tag string separator'
75
+
68
76
  include Og::EntityMixin
77
+
69
78
  many_to_many Tag
70
79
 
71
80
  # Add a tag for this object.
@@ -78,7 +87,7 @@ module Taggable
78
87
  self.tags.clear if options[:clear]
79
88
 
80
89
  for name in Taggable.tags_to_names(the_tags)
81
- Tag.find_or_create_by_name(name).tag(self)
90
+ Tag.find_or_create_by_name(name).tag(self) unless self.tagged_with?(name)
82
91
  end
83
92
  end
84
93
  alias_method :tag!, :tag
@@ -89,6 +98,12 @@ module Taggable
89
98
  tags.collect { |t| t.name }
90
99
  end
91
100
 
101
+ # Return the tag string
102
+
103
+ def tag_string(separator = Taggable.separator)
104
+ tags.collect { |t| t.name }.join(separator)
105
+ end
106
+
92
107
  # Checks to see if this object has been tagged
93
108
  # with +tag_name+.
94
109
 
@@ -125,7 +140,7 @@ module Taggable
125
140
  # UNION (OR)
126
141
 
127
142
  def find_with_any_tag(*names)
128
- info = ogmanager.store.join_table_info(self, tag)
143
+ info = ogmanager.store.join_table_info(self, Tag)
129
144
  count = names.size
130
145
  names = names.map { |n| "'#{n}'" }.join(',')
131
146
  sql = %{
@@ -158,7 +173,7 @@ module Taggable
158
173
 
159
174
  # Helper.
160
175
 
161
- def self.tags_to_names(the_tags, separator = ' ')
176
+ def self.tags_to_names(the_tags, separator = Taggable.separator)
162
177
  if the_tags.is_a? Array
163
178
  names = the_tags
164
179
  elsif the_tags.is_a? String
@@ -9,9 +9,9 @@ module Glue
9
9
  module Timestamped
10
10
  include Aspects
11
11
 
12
- property :create_time, Time, :editor => :none
13
- property :update_time, Time, :editor => :none
14
- property :access_time, Time, :editor => :none
12
+ property :create_time, Time, :control => :none
13
+ property :update_time, Time, :control => :none
14
+ property :access_time, Time, :control => :none
15
15
 
16
16
  before "@create_time = @update_time = Time.now", :on => :og_insert
17
17
  before "@update_time = Time.now", :on => :og_update
@@ -28,8 +28,8 @@ end
28
28
 
29
29
  module TimestampedOnCreate
30
30
  include Aspects
31
- property :create_time, Time, :editor => :none
32
- before "@create_time = @update_time = Time.now", :on => :og_insert
31
+ property :create_time, Time, :control => :none
32
+ before "@create_time = Time.now", :on => :og_insert
33
33
  end
34
34
 
35
35
  end
data/lib/og.rb CHANGED
@@ -43,7 +43,7 @@ module Og
43
43
 
44
44
  # The version.
45
45
 
46
- Version = '0.24.0'
46
+ Version = '0.26.0'
47
47
 
48
48
  # Library path.
49
49
 
@@ -110,8 +110,32 @@ module Og
110
110
  # as the default store.
111
111
 
112
112
  def setup(options = {:store => :sqlite})
113
- m = @@manager = Manager.new(options)
114
- m.manage_classes
113
+ begin
114
+ # This is a flag a store or manager can use to determine
115
+ # if it was being called by Og.setup to provide
116
+ # additional, faster or enhanced functionality.
117
+
118
+ puts options[:called_by_og_setup]
119
+ options[:called_by_og_setup] = true if options[:called_by_og_setup].nil?
120
+
121
+ m = @@manager = Manager.new(options)
122
+ m.manage_classes
123
+
124
+ # Allows functionality that requires a store is finalized
125
+ # to be implemented. A vastly superior method of constructing
126
+ # foreign key constraints is an example of functionality
127
+ # this provides. Currently only used by the PostgreSQL store.
128
+
129
+ m.post_setup if options[:called_by_og_setup]
130
+ rescue Exception => ex
131
+ Logger.error "Og.setup had problems: #{ex.class} => #{ex.message}"
132
+ if $DBG
133
+ Logger.error ex.inspect
134
+ Logger.error ex.backtrace.join("\n")
135
+ exit
136
+ end
137
+ end
138
+
115
139
  return m
116
140
  end
117
141
  alias_method :connect, :setup
@@ -138,4 +162,4 @@ require 'og/types'
138
162
  require 'og/validation'
139
163
  require 'og/markers'
140
164
 
141
- # * George Moschovitis <gm@navel.gr>
165
+ # * George Moschovitis <gm@navel.gr>
@@ -113,8 +113,11 @@ class Collection
113
113
  end
114
114
 
115
115
  # Add a new member to the collection.
116
+ # this method will overwrite any objects already
117
+ # existing in the collection
116
118
 
117
119
  def push(obj, options = nil)
120
+ remove(obj) if members.include?(obj)
118
121
  @members.push(obj)
119
122
  unless @building or owner.unsaved?
120
123
  @owner.send(@insert_proc, obj, options)
@@ -125,6 +128,9 @@ class Collection
125
128
 
126
129
  # Remove a member from the collection, the actual object
127
130
  # is not deleted.
131
+ #--
132
+ # TODO: add remove by oid!
133
+ #++
128
134
 
129
135
  def remove(*objects)
130
136
  objects = objects.flatten
@@ -141,6 +147,9 @@ class Collection
141
147
  end
142
148
 
143
149
  # Delete a member from the collection AND the store.
150
+ #--
151
+ # TODO: add delete by oid!
152
+ #++
144
153
 
145
154
  def delete(*objects)
146
155
  objects = objects.flatten
@@ -213,6 +222,12 @@ class Collection
213
222
  end
214
223
  end
215
224
 
225
+ # Find one object.
226
+
227
+ def find_one(options = {})
228
+ find(options).first
229
+ end
230
+
216
231
  # Redirect all other methods to the members array.
217
232
 
218
233
  def method_missing(symbol, *args, &block)
@@ -12,9 +12,13 @@ module EntityMixin
12
12
 
13
13
  def save(options = nil)
14
14
  self.class.ogmanager.store.save(self, options)
15
- # return self
16
15
  end
17
16
  alias_method :save!, :save
17
+ alias_method :validate_and_save, :save
18
+
19
+ def force_save!(options = nil)
20
+ self.class.ogmanager.store.force_save(self, options)
21
+ end
18
22
 
19
23
  def insert
20
24
  self.class.ogmanager.store.insert(self)
@@ -70,6 +74,16 @@ module EntityMixin
70
74
  end
71
75
  alias_method :assign, :assign_properties
72
76
 
77
+ # Returns a symbol => value hash of the object's
78
+ # properties.
79
+
80
+ def properties_to_hash
81
+ hash = {}
82
+ for sym, prop in self.class.properties
83
+ hash[sym] = instance_variable_get("@#{sym}")
84
+ end
85
+ end
86
+
73
87
  include RelationDSL
74
88
 
75
89
  class_inherit do
@@ -106,10 +120,16 @@ module EntityMixin
106
120
  alias_method :[], :load
107
121
  alias_method :exist?, :load
108
122
 
123
+ # Update the representation of this class in the
124
+ # store.
125
+
109
126
  def update(set, options = nil)
110
127
  ogmanager.store.update_by_sql(self, set, options)
111
128
  end
112
129
 
130
+ # Find a specific instance of this class according
131
+ # to the given conditions.
132
+
113
133
  def find(options = {})
114
134
  if find_options = self.ann.self[:find_options]
115
135
  options = find_options.dup.update(options)
@@ -122,6 +142,8 @@ module EntityMixin
122
142
  end
123
143
  alias_method :all, :find
124
144
 
145
+ # Find a single instance of this class.
146
+
125
147
  def find_one(options = {})
126
148
  options[:class] = self
127
149
  ogmanager.store.find_one(options)
@@ -137,6 +159,8 @@ module EntityMixin
137
159
  ogmanager.store.select_one(sql, self)
138
160
  end
139
161
 
162
+ # Perform a count query.
163
+
140
164
  def count(options = {})
141
165
  options[:class] = self
142
166
  ogmanager.store.count(options)
@@ -177,8 +201,13 @@ module EntityMixin
177
201
  ogmanager.store
178
202
  end
179
203
 
204
+ # Returns the primary key for this class.
205
+
180
206
  def primary_key
181
- unless pk = ann.self[:primary_key]
207
+ # gmosx: LEAVE as is, seems to be a workaround for a
208
+ # nasty bug in the current facets version.
209
+ pk = ann.self.primary_key
210
+ if pk.nil?
182
211
  pk = Entity.resolve_primary_key(self)
183
212
  ann :self, :primary_key => pk
184
213
  end
@@ -246,7 +275,8 @@ module EntityMixin
246
275
  # Set the primary key.
247
276
 
248
277
  def set_primary_key(pk, pkclass = Fixnum)
249
- ann self, :primary_key => Property.new(:symbol => pk, :klass => pkclass)
278
+ #ann self, :primary_key => Property.new(:symbol => pk, :klass => pkclass)
279
+ ann self, :primary_key => properties[pk].dup
250
280
  end
251
281
 
252
282
  # Is this entity a polymorphic parent?
@@ -323,52 +353,18 @@ module EntityMixin
323
353
 
324
354
  def method_missing(sym, *args)
325
355
  if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(sym.to_s)
326
- finder = (match.captures.first == 'all_by' ? :find : :find_one)
327
- attrs = match.captures.last.split('_and_')
328
-
329
- if args.last.is_a?(Hash)
330
- options = args.pop
331
- else
332
- options = {}
333
- end
334
-
335
- condition = []
336
- attrs.each_with_index do |a, idx|
337
- condition << %|#{a} #{options.delete("#{a}_op".to_sym) || '='} #{ogmanager.store.quote(args[idx])}|
338
- end
339
-
340
- options.update(
341
- :class => self,
342
- :condition => condition.join(' AND ')
343
- )
344
-
345
- return ogmanager.store.send(finder, options)
356
+ return finder(match, args)
346
357
  elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(sym.to_s)
347
- finder = (match.captures.first == 'all_by' ? :find : :find_one)
348
- attrs = match.captures.last.split('_and_')
349
-
350
- if args.last.is_a?(Hash)
351
- options = args.pop
352
- else
353
- options = {}
354
- end
358
+ obj = finder(match, args)
355
359
 
356
- condition = []
357
- attrs.each_with_index do |a, idx|
358
- condition << %|#{a} #{options.delete("#{a}_op".to_sym) || '='} #{ogmanager.store.quote(args[idx])}|
359
- end
360
-
361
- options.update(
362
- :class => self,
363
- :condition => condition.join(' AND ')
364
- )
365
-
366
- unless obj = ogmanager.store.send(finder, options)
360
+ unless obj
361
+ attrs = match.captures.last.split('_and_')
367
362
  obj = self.create do |obj|
368
- attrs.each_with_index do |a, idx|
369
- obj.instance_variable_set "@#{a}", args[idx]
363
+ attrs.zip(args).map do |name, value|
364
+ obj.instance_variable_set "@#{name}", value
370
365
  end
371
366
  end
367
+ yield(obj) if block_given?
372
368
  end
373
369
 
374
370
  return obj
@@ -376,6 +372,30 @@ module EntityMixin
376
372
  super
377
373
  end
378
374
  end
375
+
376
+ private
377
+
378
+ # Helper method for dynamic finders. Finds the object dynamically parsed
379
+ # method name is after.
380
+
381
+ def finder(match, args)
382
+ finder = (match.captures.first == 'all_by' ? :find : :find_one)
383
+ attrs = match.captures.last.split('_and_')
384
+
385
+ options = {}
386
+ options = args.pop if args.last.is_a?(Hash)
387
+
388
+ condition = attrs.zip(args).map do |name, value|
389
+ %|#{name} #{options.delete("#{name}_op".to_sym) || '='} #{ogmanager.store.quote(value)}|
390
+ end.join(' AND ')
391
+
392
+ options.update(
393
+ :class => self,
394
+ :condition => condition
395
+ )
396
+
397
+ return ogmanager.store.send(finder, options)
398
+ end
379
399
 
380
400
  end
381
401