og 0.25.0 → 0.26.0

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