plain_record 0.3 → 0.4

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/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  language: ruby
2
+ bundler_args: --without development
2
3
  rvm:
3
4
  - 1.8.7
4
- - ruby-head
5
+ - 1.9.3
5
6
  - jruby-18mode
7
+ - rbx-18mode
8
+ - rbx-19mode
data/ChangeLog CHANGED
@@ -1,3 +1,8 @@
1
+ == 0.4 (Phosgene)
2
+ * Allow to store images.
3
+ * Allow to store field in separated file.
4
+ * Fix JRuby and Rubinius support.
5
+
1
6
  == 0.3 (Chlorine)
2
7
  * Use modules to use super in filters.
3
8
  * Add filter to i18n support.
data/Gemfile CHANGED
@@ -1,8 +1,13 @@
1
1
  source :rubygems
2
2
 
3
3
  gem 'rake'
4
- gem 'yard'
5
4
  gem 'rspec'
6
- gem 'redcarpet'
7
- gem 'r18n-core', :require => nil, :github => 'ai/r18n'
5
+ gem 'r18n-core', :require => nil
8
6
  gem 'i18n', :require => nil
7
+ gem 'rmagick', :platforms => :ruby
8
+ gem 'rmagick4j', :platforms => :jruby
9
+
10
+ group :development do
11
+ gem 'redcarpet'
12
+ gem 'yard'
13
+ end
data/Gemfile.lock CHANGED
@@ -1,33 +1,31 @@
1
- GIT
2
- remote: git://github.com/ai/r18n.git
3
- revision: ca6933926eb955344eeb982de35f492c76a38b1a
4
- specs:
5
- r18n-core (0.4.14)
6
-
7
1
  GEM
8
2
  remote: http://rubygems.org/
9
3
  specs:
10
4
  diff-lcs (1.1.3)
11
- i18n (0.6.0)
12
- rake (0.9.2.2)
13
- redcarpet (2.1.1)
14
- rspec (2.10.0)
15
- rspec-core (~> 2.10.0)
16
- rspec-expectations (~> 2.10.0)
17
- rspec-mocks (~> 2.10.0)
18
- rspec-core (2.10.1)
19
- rspec-expectations (2.10.0)
5
+ i18n (0.6.1)
6
+ r18n-core (1.1.3)
7
+ rake (10.0.3)
8
+ redcarpet (2.2.2)
9
+ rmagick (2.13.1)
10
+ rspec (2.12.0)
11
+ rspec-core (~> 2.12.0)
12
+ rspec-expectations (~> 2.12.0)
13
+ rspec-mocks (~> 2.12.0)
14
+ rspec-core (2.12.2)
15
+ rspec-expectations (2.12.1)
20
16
  diff-lcs (~> 1.1.3)
21
- rspec-mocks (2.10.1)
22
- yard (0.8.1)
17
+ rspec-mocks (2.12.1)
18
+ yard (0.8.3)
23
19
 
24
20
  PLATFORMS
25
21
  ruby
26
22
 
27
23
  DEPENDENCIES
28
24
  i18n
29
- r18n-core!
25
+ r18n-core
30
26
  rake
31
27
  redcarpet
28
+ rmagick
29
+ rmagick4j
32
30
  rspec
33
31
  yard
data/Rakefile CHANGED
@@ -15,10 +15,12 @@ require 'rspec/core/rake_task'
15
15
 
16
16
  RSpec::Core::RakeTask.new
17
17
 
18
- require 'yard'
19
- YARD::Rake::YardocTask.new do |yard|
20
- yard.options << "--title='Plain Record #{PlainRecord::VERSION}'"
21
- end
18
+ begin
19
+ require 'yard'
20
+ YARD::Rake::YardocTask.new do |yard|
21
+ yard.options << "--title='Plain Record #{PlainRecord::VERSION}'"
22
+ end
23
+ rescue LoadError; end
22
24
 
23
25
  task :clobber_doc do
24
26
  rm_r 'doc' rescue nil
@@ -88,7 +88,7 @@ module PlainRecord
88
88
  #
89
89
  # will be store as:
90
90
  #
91
- # author: John Smith
91
+ # author: John Smith
92
92
  # movie:
93
93
  # title: Watchmen
94
94
  # genre: action
@@ -114,8 +114,8 @@ module PlainRecord
114
114
  map = Associations.map(model, klass, "#{field}_") if map.empty?
115
115
  Associations.define_link_one(model, klass, field, map)
116
116
  else
117
- raise ArgumentError, "You couldn't create association field" +
118
- " #{field} by text creator"
117
+ raise ArgumentError,
118
+ "You couldn't create association field #{field} by text creator"
119
119
  end
120
120
  end
121
121
  end
@@ -133,8 +133,8 @@ module PlainRecord
133
133
  map = Associations.map(klass, model, prefix) if map.empty?
134
134
  Associations.define_link_many(model, klass, field, map)
135
135
  else
136
- raise ArgumentError, "You couldn't create association field" +
137
- " #{field} by text creator"
136
+ raise ArgumentError,
137
+ "You couldn't create association field #{field} by text creator"
138
138
  end
139
139
  end
140
140
  end
@@ -144,16 +144,22 @@ module PlainRecord
144
144
  def define_real_one(klass, field, model)
145
145
  name = field.to_s
146
146
  klass.after :load do |result, entry|
147
- entry.data[name] = model.new(entry.file, entry.data[name])
147
+ if entry.data[name]
148
+ entry.data[name] = model.new(entry.file, entry.data[name])
149
+ end
148
150
  result
149
151
  end
150
152
  klass.before :save do |entry|
151
- model.call_before_callbacks(:save, [entry.data[name]])
152
- entry.data[name] = entry.data[name]
153
+ if entry.data[name]
154
+ model.call_before_callbacks(:save, [entry.data[name]])
155
+ entry.data[name] = entry.data[name]
156
+ end
153
157
  end
154
158
  klass.after :save do |result, entry|
155
- entry.data[name] = model.new(entry.file, entry.data[name])
156
- model.call_after_callbacks(:save, nil, [entry.data[name]])
159
+ if entry.data[name]
160
+ entry.data[name] = model.new(entry.file, entry.data[name])
161
+ model.call_after_callbacks(:save, nil, [entry.data[name]])
162
+ end
157
163
  result
158
164
  end
159
165
  end
@@ -35,10 +35,8 @@ module PlainRecord::Extra
35
35
  # virtual :updated_at, git_modify_time
36
36
  # end
37
37
  module Git
38
- class << self
39
- def included(base)
40
- base.send :extend, Model
41
- end
38
+ def self.included(base)
39
+ base.send :extend, Model
42
40
  end
43
41
 
44
42
  # Return time of first commit of model file (created time).
@@ -67,8 +65,6 @@ module PlainRecord::Extra
67
65
 
68
66
  module Model
69
67
 
70
- private
71
-
72
68
  # Filter to set default value to time of last file git commit.
73
69
  # If file is not commited or has changes, filter will return `Time.now`.
74
70
  def git_modified_time
@@ -47,10 +47,8 @@ module PlainRecord::Extra
47
47
  # end
48
48
  # end
49
49
  module I18n
50
- class << self
51
- def included(base)
52
- base.send :extend, Model
53
- end
50
+ def self.included(base)
51
+ base.send :extend, Model
54
52
  end
55
53
 
56
54
  # Return default locale. By default it look in R18n or Rails I18n.
@@ -0,0 +1,282 @@
1
+ =begin
2
+ Extention to store images.
3
+
4
+ Copyright (C) 2012 Andrey “A.I.” Sitnik <andrey@sitnik.ru>,
5
+ sponsored by Evil Martians.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Lesser General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Lesser General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Lesser General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ require 'fileutils'
22
+
23
+ module PlainRecord::Extra
24
+ # Extention to store images.
25
+ #
26
+ # It make sense only with `entry_in` models. You can get created or modified
27
+ # time from first or last git commit time.
28
+ #
29
+ # It is additional extention, so you need to include `PlainRecord::Extra::Git`
30
+ # module to your model.
31
+ #
32
+ # class User
33
+ # include PlainRecord::Resource
34
+ # include PlainRecord::Extra::Image
35
+ #
36
+ # entry_in "users/*.yml"
37
+ #
38
+ # image_from do |user, field|
39
+ # "users/#{field}/#{user.name}.png"
40
+ # end
41
+ # image_url do |user, field, size|
42
+ # if size
43
+ # "users/#{user.name}/#{field}.#{size}.png"
44
+ # else
45
+ # "users/#{user.name}/#{field}.png"
46
+ # end
47
+ # end
48
+ #
49
+ # virtual :name, in_filepath(1)
50
+ # virtual :avatar, image(small: '32x32', big: '64x64')
51
+ # virtual :photo, image
52
+ # end
53
+ #
54
+ # # There are images at `data/users/avatar/ai.png` and
55
+ # # `data/users/photo/ai.png`
56
+ #
57
+ # PlainRecord::Extra::Image.convert_images!
58
+ # user = User.first(name: 'ai')
59
+ #
60
+ # user.photo.url #=> "data/users/ai/photo.png"
61
+ # user.photo.file #=> "app/assets/images/data/users/ai/avatar.small.png"
62
+ # user.avatar(:small).url #=> "data/users/ai/avatar.small.png"
63
+ module Image
64
+ class << self
65
+ # Models, that used this extention.
66
+ attr_accessor :included_in
67
+
68
+ # Base of converted images.
69
+ # Set to <tt>app/aseets/images/data</tt> in Rails.
70
+ attr_accessor :dir
71
+
72
+ # Base of converted images URL. Set to <tt>data/</tt> in Rails.
73
+ attr_accessor :url
74
+
75
+ def included(base)
76
+ base.send :extend, Model
77
+ self.included_in ||= []
78
+ self.included_in << base
79
+ end
80
+
81
+ # Define class variables.
82
+ def install(klass)
83
+ klass.image_sizes = { }
84
+ end
85
+
86
+ def dir
87
+ unless @dir
88
+ raise ArgumentError,
89
+ 'You need to set `PlainRecord::Extra::Image.dir`'
90
+ end
91
+ @dir
92
+ end
93
+
94
+ def url
95
+ unless @url
96
+ raise ArgumentError,
97
+ 'You need to set `PlainRecord::Extra::Image.url`'
98
+ end
99
+ @url
100
+ end
101
+
102
+ # Delete all converted images.
103
+ def clean_images!
104
+ Dir.glob(File.join(self.dir, '**/*')) do |file|
105
+ FileUtils.rm_r(file) if File.exists? file
106
+ end
107
+ end
108
+
109
+ # Convert images in all models.
110
+ def convert_images!
111
+ clean_images!
112
+ return unless included_in
113
+
114
+ included_in.each do |model|
115
+ model.all.each { |i| i.convert_images! }
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ # Convert all images to public dir.
122
+ def convert_images!
123
+ self.class.image_sizes.each_pair do |field, sizes|
124
+ from = self.class.get_image_from(self, field)
125
+ next unless File.exists? from
126
+
127
+ if sizes.empty?
128
+ to = self.class.get_image_file(self, field, nil)
129
+ FileUtils.mkpath(File.dirname(to))
130
+ FileUtils.cp(from, to)
131
+ else
132
+ require 'RMagick'
133
+ source = ::Magick::Image.read(from).first
134
+ sizes.each_pair do |name, size|
135
+ to = self.class.get_image_file(self, field, name)
136
+ w, h = size.split('x')
137
+ image = source.resize(w.to_i, h.to_i)
138
+ self.class.use_callbacks(:convert_image, self, to) do
139
+ FileUtils.mkpath(File.dirname(to))
140
+ image.write(to)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ # Field value object with image paths and URL.
148
+ class Data
149
+
150
+ # Name of image size.
151
+ attr_reader :size_name
152
+
153
+ # Format of image size.
154
+ attr_reader :size
155
+
156
+ # Image width.
157
+ attr_reader :width
158
+
159
+ # Image height.
160
+ attr_reader :height
161
+
162
+ # Converted image URL.
163
+ attr_reader :url
164
+
165
+ # Converted image file.
166
+ attr_reader :file
167
+
168
+ # Original image file.
169
+ attr_reader :original
170
+
171
+ # Set image paths.
172
+ def initialize(entry, field, size)
173
+ @size_name = size
174
+ @original = entry.class.get_image_from(entry, field)
175
+
176
+ if size
177
+ @size = entry.class.image_sizes[field][size]
178
+ unless @size
179
+ raise ArgumentError, "Field `#{field}` doesn't have `#{size}` size"
180
+ end
181
+ @width, @height = @size.split('x').map { |i| i.to_i }
182
+ end
183
+
184
+ if size or entry.class.image_sizes[field].empty?
185
+ @url = entry.class.get_image_url(entry, field, size)
186
+ @file = entry.class.get_image_file(entry, field, size)
187
+ end
188
+ end
189
+
190
+ def exists?
191
+ File.exists? @original
192
+ end
193
+
194
+ end
195
+
196
+ module Model
197
+
198
+ # Hash of image sizes.
199
+ attr_accessor :image_sizes
200
+
201
+ # Set source image path.
202
+ def image_from(&block)
203
+ @image_from = block
204
+ end
205
+
206
+ # Set relative image URL path from +image_base+.
207
+ def image_url(&block)
208
+ @image_url = block
209
+ end
210
+
211
+ # Get source image path.
212
+ def get_image_from(entry, field)
213
+ PlainRecord.root(@image_from.call(entry, field))
214
+ end
215
+
216
+ # Get converted image path.
217
+ def get_image_file(entry, field, size)
218
+ unless @image_url
219
+ raise ArgumentError,
220
+ "You need to set `image_url` in #{to_s} to calculate file path."
221
+ end
222
+ File.join(Image.dir, @image_url.call(entry, field, size))
223
+ end
224
+
225
+ # Get relative image URL path.
226
+ def get_image_url(entry, field, size)
227
+ unless @image_url
228
+ raise ArgumentError,
229
+ "You need to set `image_url` in #{to_s} to calculate file path."
230
+ end
231
+ File.join(Image.url, @image_url.call(entry, field, size))
232
+ end
233
+
234
+ private
235
+
236
+ # Use pngcrush (you must install it in system) to optimize png images.
237
+ #
238
+ # class User
239
+ # include PlainRecord::Resource
240
+ # include PlainRecord::Extra::Image
241
+ #
242
+ # optimize_png
243
+ # …
244
+ # end
245
+ def optimize_png
246
+ after :convert_image do |result, entry, file|
247
+ file = file.gsub(/([^A-Za-z0-9_\-\.,:\/@])/, "\\\\\\1")
248
+ file = file.gsub(/(^|\/)\.?\.($|\/)/, '')
249
+ return unless file =~ /\.png$/
250
+
251
+ tmp = file + '.optimized'
252
+ `pngcrush -rem gAMA -rem cHRM -rem iCCP -rem sRGB "#{file}" "#{tmp}"`
253
+ FileUtils.rm(file)
254
+ FileUtils.mv(tmp, file)
255
+ end
256
+ end
257
+
258
+ # Filter to create field with image. You must set +image_from+.
259
+ #
260
+ # If you create Rails application you must set also +image_url+.
261
+ # If you create web application with another web framework, you must
262
+ # also redefine `image_url_to_path` or set `image_to`.
263
+ #
264
+ # If you create non-web application, you must set only `image_to`.
265
+ #
266
+ # You can set sizes hash (name to ImageMagick size) to convert images.
267
+ def image(sizes = { })
268
+ proc do |model, name, type|
269
+ Image.install(model) unless model.image_sizes
270
+ model.image_sizes[name] = sizes
271
+
272
+ model.add_accessors <<-EOS, __FILE__, __LINE__
273
+ def #{name}(size = nil)
274
+ PlainRecord::Extra::Image::Data.new(self, :#{name}, size)
275
+ end
276
+ EOS
277
+ end
278
+ end
279
+
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,91 @@
1
+ =begin
2
+ Extention to set to get field value from external file.
3
+
4
+ Copyright (C) 2012 Andrey “A.I.” Sitnik <andrey@sitnik.ru>,
5
+ sponsored by Evil Martians.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Lesser General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Lesser General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Lesser General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ module PlainRecord
22
+ # Extention to set to get field value from external file.
23
+ #
24
+ # class Post
25
+ # include PlainRecord::Resource
26
+ #
27
+ # entry_in '*/post.md'
28
+ #
29
+ # virtual :name, in_filepath(1)
30
+ # virtuel :text, file { |p| "#{p.name}/text.#{I18n.locale}.md" }
31
+ # end
32
+ module File
33
+ # Cache of field content before save.
34
+ attr_accessor :unsaved_files
35
+
36
+ # Return file pathname for fiel field.
37
+ def field_filepath(field)
38
+ path = self.class.fields_files[field]
39
+ path = path.call(self) if path.is_a? Proc
40
+ PlainRecord.root(path)
41
+ end
42
+
43
+ private
44
+
45
+ # Define class variables and events in +klass+. It should be call once.
46
+ def self.install(klass)
47
+ klass.fields_files = { }
48
+
49
+ klass.before :load do |entry|
50
+ entry.unsaved_files = { }
51
+ end
52
+
53
+ klass.before :save do |entry|
54
+ entry.unsaved_files.each_pair do |file, value|
55
+ ::File.open(file, 'w') { |io| io << value; }
56
+ end
57
+ entry.unsaved_files = { }
58
+ end
59
+ end
60
+
61
+ module Model
62
+
63
+ # Field file paths.
64
+ attr_accessor :fields_files
65
+
66
+ private
67
+
68
+ # Filter to load field value from file.
69
+ def file(path = nil, &block)
70
+ proc do |model, field, type|
71
+ File.install(model) unless model.fields_files
72
+ model.fields_files[field] = block_given? ? block : path
73
+
74
+ model.add_accessors <<-EOS, __FILE__, __LINE__
75
+ def #{field}
76
+ path = field_filepath(:#{field})
77
+ return @unsaved_files[path] if @unsaved_files.has_key? path
78
+ return nil unless ::File.exists? path
79
+ ::File.read(path)
80
+ end
81
+
82
+ def #{field}=(value)
83
+ @unsaved_files[field_filepath(:#{field})] = value
84
+ end
85
+ EOS
86
+ end
87
+ end
88
+
89
+ end
90
+ end
91
+ end
@@ -59,8 +59,8 @@ module PlainRecord
59
59
  def in_filepath(number)
60
60
  proc do |model, field, type|
61
61
  if :virtual != type
62
- raise ArgumentError, "You must create filepath field #{field}" +
63
- ' virtual creator'
62
+ raise ArgumentError,
63
+ "You must create filepath field #{field} virtual creator"
64
64
  end
65
65
 
66
66
  Filepath.install(model) unless model.filepath_fields
@@ -77,57 +77,56 @@ module PlainRecord
77
77
  end
78
78
  end
79
79
 
80
- class << self
81
- # Define class variables and events in +klass+. It should be call once on
82
- # same class after +entry_in+ or +list_in+ call.
83
- def install(klass)
84
- klass.filepath_fields = { }
80
+ # Define class variables and events in +klass+. It should be call once on
81
+ # same class after +entry_in+ or +list_in+ call.
82
+ def self.install(klass)
83
+ klass.filepath_fields = { }
85
84
 
86
- path = Regexp.escape(klass.path).gsub(/\\\*\\\*(\/|$)/, '(.*)').
87
- gsub('\\*', '([^/]+)')
88
- klass.filepath_regexp = Regexp.new(path)
85
+ path = Regexp.escape(klass.path).gsub(/\\\*\\\*(\/|$)/, '(.*)').
86
+ gsub('\\*', '([^/]+)')
87
+ klass.filepath_regexp = Regexp.new(path)
89
88
 
90
- klass.class_eval do
91
- attr_accessor :filepath_data
92
- end
89
+ klass.class_eval do
90
+ attr_accessor :filepath_data
91
+ end
93
92
 
94
- klass.after :load do |result, entry|
95
- if entry.path
96
- data = klass.filepath_regexp.match(entry.path)
97
- entry.filepath_data = { }
98
- klass.filepath_fields.each_pair do |number, name|
99
- entry.filepath_data[name] = data[number]
100
- end
101
- else
102
- entry.filepath_data = { }
103
- klass.filepath_fields.each_value do |name|
104
- entry.filepath_data[name] = entry.data[name]
105
- entry.data.delete(name)
106
- end
93
+ klass.after :load do |result, entry|
94
+ if entry.path
95
+ data = klass.filepath_regexp.match(entry.path)
96
+ entry.filepath_data = { }
97
+ klass.filepath_fields.each_pair do |number, name|
98
+ entry.filepath_data[name] = data[number]
99
+ end
100
+ else
101
+ entry.filepath_data = { }
102
+ klass.filepath_fields.each_value do |name|
103
+ entry.filepath_data[name] = entry.data[name]
104
+ entry.data.delete(name)
107
105
  end
108
- result
109
106
  end
107
+ result
108
+ end
110
109
 
111
- klass.after :path do |path, matchers|
112
- i = 0
113
- path.gsub /(\*\*(\/|$)|\*)/ do |pattern|
114
- i += 1
115
- field = klass.filepath_fields[i]
116
- unless matchers[field].is_a? Regexp or matchers[field].nil?
117
- matchers[field]
118
- else
119
- pattern
120
- end
110
+ klass.after :path do |path, matchers|
111
+ i = 0
112
+ path.gsub /(\*\*(\/|$)|\*)/ do |pattern|
113
+ i += 1
114
+ field = klass.filepath_fields[i]
115
+ unless matchers[field].is_a? Regexp or matchers[field].nil?
116
+ matchers[field]
117
+ else
118
+ pattern
121
119
  end
122
120
  end
121
+ end
123
122
 
124
- klass.before :save do |entry|
125
- unless entry.file
126
- path = klass.path(entry.filepath_data)
127
- entry.file = path unless path =~ /[\*\[\?\{]/
128
- end
123
+ klass.before :save do |entry|
124
+ unless entry.file
125
+ path = klass.path(entry.filepath_data)
126
+ entry.file = path unless path =~ /[\*\[\?\{]/
129
127
  end
130
128
  end
131
129
  end
130
+
132
131
  end
133
132
  end