plain_record 0.3 → 0.4

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