photo_folder 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,2 @@
1
+ PhotoFolder Gem
2
+ ===============
@@ -0,0 +1,115 @@
1
+ class Photo < ActiveRecord::Base
2
+ belongs_to :photo_collection
3
+ has_many :photo_taggings, :dependent => :destroy
4
+ has_many :photo_tags, :through => :photo_taggings
5
+ acts_as_list :scope => :photo_collection
6
+ before_create :set_exif_data, :set_last_modified, :set_guid, :set_nsfw
7
+ after_create :set_tags
8
+
9
+ def self.guid(path,date)
10
+ Digest::MD5.hexdigest([
11
+ path,
12
+ date
13
+ ].join(''))
14
+ end
15
+
16
+ def full_path
17
+ File.join(PhotoFolder.source_directory_location,path)
18
+ end
19
+
20
+ def update_meta
21
+ set_exif_data
22
+ set_last_modified
23
+ set_guid
24
+ set_nsfw
25
+ save
26
+ set_tags
27
+ end
28
+
29
+ def set_guid
30
+ self.guid = Photo.guid(path,date)
31
+ end
32
+
33
+ def set_tags
34
+ #@tags was set by set_exif_data, which is always called first
35
+ self.photo_taggings.destroy_all
36
+ @tags.each do |str|
37
+ self.photo_taggings.create({
38
+ :photo_tag_id => (PhotoTag.find_by_tag(str) || PhotoTag.create(:tag => str)).id,
39
+ :photo_id => self.id
40
+ })
41
+ end
42
+ end
43
+
44
+ def set_nsfw
45
+ self.nsfw = @tags.collect(&:downcase).include?('nsfw') ? 1 : 0
46
+ end
47
+
48
+ def set_last_modified
49
+ self.last_modified = File.mtime(full_path)
50
+ end
51
+
52
+ def set_exif_data
53
+ begin
54
+ exif = EXIFData.new(MiniExiftool.new(full_path),logger)
55
+
56
+ [:focal_length,:iso,:shutter_speed,:f_stop,:camera,:caption,:location,:height,:width].each do |key|
57
+ self.send(key.to_s + '=',exif[key] || nil)
58
+ end
59
+
60
+ #date,title,lens and tags are special cases
61
+ if exif[:date].is_a? Time
62
+ self.date = exif[:date]
63
+ else
64
+ date_match = (exif[:date] || '').match(/([\d]+):([\d]+):([\d]+) ([\d]+):([\d]+):([\d]+)/)
65
+ self.date = Time.mktime(date_match[1],date_match[2],date_match[3],date_match[4],date_match[5],date_match[6]) if date_match
66
+ end
67
+ self.name = exif[:title] || full_path.split('/').pop.gsub(/\.[a-zA-Z]{1,4}/,'')
68
+ self.lens = exif[:lens] ? exif[:lens] : nil
69
+ @tags = exif[:tags] || []
70
+ rescue
71
+ self.name = full_path.split('/').pop.gsub(/\.[a-zA-Z]{1,4}/,'')
72
+ @tags = []
73
+ end
74
+ end
75
+
76
+ class EXIFData
77
+ EXIF_TRANSLATIONS = {
78
+ :title => ['Title','ObjectName'],
79
+ :focal_length => ['FocalLength'],
80
+ :iso => ['ISO'],
81
+ :height => ['ImageHeight'],
82
+ :width => ['ImageWidth'],
83
+ :shutter_speed => ['ShutterSpeedValue'],
84
+ :f_stop => ['ApertureValue'],
85
+ :lens => ['Lens','LensType'],
86
+ :camera => ['Model'],
87
+ :date => ['DateTimeOriginal'],
88
+ :caption => ['Description','ImageDescription','Caption-Abstract'],
89
+ :tags => ['Keywords','Subject'],
90
+ :location => ['Location'],
91
+ }
92
+
93
+ EXIF_TRANSFORMATIONS = {
94
+ #:date => lambda{|exif_value| exif_value.to_s.split('-').shift},
95
+ :shutter_speed => lambda{|exif_value| exif_value.to_s.gsub(/1\//,'')},
96
+ :focal_length => lambda{|exif_value| exif_value.to_s.gsub(/\.([\d]{1,})?\s?.+$/,'')},
97
+ :tags => lambda{|exif_value| (exif_value.is_a?(Array) ? exif_value.join(',') : exif_value).gsub(/["']+/,'').gsub(/\s+?,\s+?/,',').split(',').collect(&:strip).collect(&:downcase)}
98
+ }
99
+
100
+ def initialize(exif_data,logger)
101
+ @exif_data = exif_data
102
+ @logger = logger
103
+ end
104
+
105
+ def [](key)
106
+ EXIF_TRANSLATIONS[key.to_sym].each do |exif_key_name|
107
+ exif_value = @exif_data[exif_key_name.to_s]
108
+ if !exif_value.nil? && exif_value != false && exif_value != '' && exif_value != 'false' && exif_value != 'nil'
109
+ return EXIF_TRANSFORMATIONS[key] ? EXIF_TRANSFORMATIONS[key].call(exif_value) : exif_value
110
+ end
111
+ end
112
+ false
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,96 @@
1
+ class PhotoCollection < ActiveRecord::Base
2
+ has_many :photos, :order => 'position'
3
+ acts_as_list :scope => :parent_id
4
+ acts_as_tree :order => :name
5
+ after_create :set_children_parent_id
6
+ before_create :set_name
7
+
8
+ def self.set_correct_positions(collections,is_callback = false)
9
+ collections.sort_by(&:name).each_with_index do |collection,i|
10
+ collection.update_attribute :position, i
11
+ set_correct_positions(PhotoCollection.find_all_by_parent_id(collection.id),true)
12
+ end
13
+ if self.positions && !is_callback
14
+ self.positions.each_with_index do |file_name,i|
15
+ photo_collection = PhotoCollection.find_by_path(file_name,{
16
+ :conditions => {
17
+ :parent_id => 0
18
+ }
19
+ })
20
+ if photo_collection
21
+ photo_collection.insert_at i + 1
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.positions_file_path
28
+ File.join(PhotoFolder.source_directory_location,'positions.yml')
29
+ end
30
+
31
+ def self.positions
32
+ return @positions if !@positions.nil?
33
+ if File.exists?(self.positions_file_path)
34
+ @positions = YAML.load(File.read(self.positions_file_path))
35
+ else
36
+ nil
37
+ end
38
+ end
39
+
40
+ def set_children_parent_id
41
+ PhotoFolder::all_paths_in_database.select{|path| self.path.underscore.gsub(/\s/,'_') == path.gsub(/\/[^\/]+$/,'').underscore.gsub(/\s/,'_')}.each do |path|
42
+ Photo.update_all("photo_collection_id = #{self.id}","path = '#{path}'")
43
+ end
44
+ end
45
+
46
+ def set_parent_id
47
+ update_attribute :parent_id, parent_from_path ? parent_from_path.id : 0
48
+ end
49
+
50
+ def set_name
51
+ self.name = path.split('/').pop.gsub(/[\_\-]+/,' ').split(/\s+/).each(&:capitalize!).join(' ')
52
+ end
53
+
54
+ def parent_from_path
55
+ self.path.count('/') > 0 ? PhotoCollection.find_by_path(self.path.gsub(/\/[^\/]+$/,'')) : false
56
+ end
57
+
58
+ def full_path
59
+ File.join(PhotoFolder.source_directory_location,path)
60
+ end
61
+
62
+ def positions_file_path
63
+ File.join(full_path,'positions.yml')
64
+ end
65
+
66
+ def positions
67
+ return @positions if !@positions.nil?
68
+ if File.exists?(positions_file_path)
69
+ @positions = YAML.load(File.read(positions_file_path))
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ def meta_file_path
76
+ File.join(full_path,'meta.json')
77
+ end
78
+
79
+ def set_meta
80
+ meta_data = File.exists?(meta_file_path) ? File.read(meta_file_path) : false
81
+ write_attribute :meta, meta_data
82
+ meta_data
83
+ end
84
+
85
+ def set_correct_positions
86
+ positions.each_with_index do |file_name,i|
87
+ photo = photos.find_by_path("#{path}/#{file_name}")
88
+ photo_collection = children.find_by_path("#{path}/#{file_name}")
89
+ if photo
90
+ photo.insert_at i + 1
91
+ elsif photo_collection
92
+ photo_collection.insert_at i + 1
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ class PhotoTag < ActiveRecord::Base
2
+ has_many :photos, :through => :photo_taggings
3
+ end
@@ -0,0 +1,4 @@
1
+ class PhotoTagging < ActiveRecord::Base
2
+ belongs_to :photo_tag
3
+ belongs_to :photo
4
+ end
@@ -0,0 +1,296 @@
1
+ #external libraries
2
+ require 'rubygems'
3
+ gem 'sqlite3-ruby'
4
+ gem 'activerecord'
5
+ require 'activerecord'
6
+ require 'fileutils'
7
+ require 'tempfile'
8
+ require 'pstore'
9
+ require 'set'
10
+ require 'ftools'
11
+ require 'digest/md5'
12
+ require 'yaml'
13
+
14
+ #internal libraries
15
+ require 'lib/vendor/acts_as_tree'
16
+ require 'lib/vendor/acts_as_list'
17
+ require 'lib/vendor/mini_exiftool'
18
+ require 'lib/models/photo'
19
+ require 'lib/models/photo_tag'
20
+ require 'lib/models/photo_tagging'
21
+ require 'lib/models/photo_collection'
22
+
23
+ unless Numeric.instance_methods.include? 'to_a'
24
+ class Numeric
25
+ def to_a
26
+ [self]
27
+ end
28
+ end
29
+ end
30
+
31
+ module PhotoFolder
32
+ def self.setup_database(database_location)
33
+ ActiveRecord::Base.establish_connection(
34
+ :adapter => "sqlite3",
35
+ :database => database_location,
36
+ :timeout => 5000
37
+ )
38
+ PhotoFolder::create_schema
39
+ end
40
+
41
+ def self.create_schema
42
+ if !Photo.table_exists?
43
+ ActiveRecord::Base.connection.create_table("photos") do |t|
44
+ t.integer "photo_collection_id"
45
+ t.integer "position"
46
+ t.integer "height"
47
+ t.integer "width"
48
+ t.string "guid"
49
+ t.string "path"
50
+ t.string "name"
51
+ t.datetime "date"
52
+ t.string "location"
53
+ t.string "f_stop"
54
+ t.string "shutter_speed"
55
+ t.string "focal_length"
56
+ t.string "iso"
57
+ t.string "camera"
58
+ t.string "lens"
59
+ t.text "caption"
60
+ t.datetime "last_modified"
61
+ t.integer "nsfw"
62
+ t.integer "featured"
63
+ end
64
+ end
65
+
66
+ if !PhotoCollection.table_exists?
67
+ ActiveRecord::Base.connection.create_table("photo_collections") do |t|
68
+ t.string "name"
69
+ t.string "path"
70
+ t.integer "parent_id"
71
+ t.integer "position"
72
+ t.text "meta"
73
+ end
74
+ end
75
+
76
+ if !PhotoTag.table_exists?
77
+ ActiveRecord::Base.connection.create_table("photo_tags") do |t|
78
+ t.string "tag"
79
+ end
80
+ end
81
+
82
+ if !PhotoTagging.table_exists?
83
+ ActiveRecord::Base.connection.create_table("photo_taggings") do |t|
84
+ t.integer "photo_tag_id"
85
+ t.integer "photo_id"
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.photos_list
91
+ return @photos_list if !@photos_list.nil?
92
+ @photos_list = Dir[PhotoFolder.source_directory_location + '/**/*.{png,jpg}']
93
+ end
94
+
95
+ def self.all_paths_in_database
96
+ return @all_paths_in_database if !@all_paths_in_database.nil?
97
+ @all_paths_in_database = Photo.find(:all).collect(&:path)
98
+ end
99
+
100
+ def self.all_paths_in_filesystem
101
+ return @all_paths_in_filesystem if !@all_paths_in_filesystem.nil?
102
+ @all_paths_in_filesystem = PhotoFolder::photos_list.collect{|file| file.gsub(PhotoFolder.source_directory_location + '/','')}
103
+ end
104
+
105
+ def self.flush_cached_path_information
106
+ @all_paths_in_filesystem = nil
107
+ @all_paths_in_database = nil
108
+ end
109
+
110
+ def self.scan_photos
111
+ to_add = []
112
+ to_update = []
113
+ to_remove = []
114
+
115
+ PhotoFolder::all_paths_in_filesystem.each do |path_in_filesystem|
116
+ to_add.push(path_in_filesystem) if !PhotoFolder::all_paths_in_database.select{|path_in_database| path_in_database == path_in_filesystem}[0]
117
+ end
118
+
119
+ PhotoFolder::all_paths_in_database.each do |path_in_database|
120
+ to_remove.push(path_in_database) if !PhotoFolder::all_paths_in_filesystem.select{|path_in_filesystem| path_in_filesystem == path_in_database}[0]
121
+ end
122
+
123
+ PhotoFolder::all_paths_in_filesystem.each do |path_in_filesystem|
124
+ if !to_add.include?(path_in_filesystem) && !to_remove.include?(path_in_filesystem)
125
+ entry = Photo.find_by_path(path_in_filesystem)
126
+ if entry && File.mtime(entry.full_path) > entry.last_modified
127
+ to_update.push(path_in_filesystem)
128
+ end
129
+ elsif to_add.include?(path_in_filesystem)
130
+ #cleaned = Photo.clean_filename(path_in_filesystem)
131
+ entry = Photo.find_by_path(path_in_filesystem)
132
+ if entry
133
+ to_update.push(path_in_filesystem)
134
+ end
135
+ end
136
+ end
137
+
138
+ to_add.reject!{|item| to_update.include?(item)}
139
+
140
+ [to_add,to_update,to_remove]
141
+ end
142
+
143
+ def self.scan_photo_collections
144
+ collection_paths_in_database = PhotoCollection.find(:all).collect(&:path)
145
+
146
+ collection_paths_in_filesystem = []
147
+ PhotoFolder::all_paths_in_database.each do |path|
148
+ collection = path.gsub(/\/[^\/]+$/,'')
149
+ collection_bits = collection.split('/')
150
+ collection_bits.each_with_index do |part,i|
151
+ joined = collection_bits[0,i].join('/')
152
+ collection_paths_in_filesystem.push(joined) if !collection_paths_in_filesystem.include?(joined) && joined != ''
153
+ end
154
+ collection_paths_in_filesystem.push(collection) if !collection_paths_in_filesystem.include?(collection)
155
+ end
156
+
157
+ to_add = []
158
+ to_remove = []
159
+
160
+ collection_paths_in_filesystem.each do |path_in_filesystem|
161
+ to_add.push(path_in_filesystem) if !collection_paths_in_database.include?(path_in_filesystem)
162
+ end
163
+
164
+ collection_paths_in_database.each do |path_in_database|
165
+ to_remove.push(path_in_database) if !collection_paths_in_filesystem.include?(path_in_database)
166
+ end
167
+
168
+ [to_add,to_remove]
169
+ end
170
+
171
+ def self.hash_from_database
172
+ {
173
+ :photo_collections => PhotoCollection,
174
+ :photos => Photo,
175
+ :photo_tags => PhotoTag,
176
+ :photo_taggings => PhotoTagging
177
+ }.inject(Hash.new) do |result,key_name_and_model|
178
+ key_name = key_name_and_model[0]
179
+ model = key_name_and_model[1]
180
+ result[key_name] = model.find(:all).inject(Hash.new) do |inner_result,item|
181
+ inner_result[item.id.to_s] = item.attributes.inject(Hash.new) do |attribute_result,key_and_value|
182
+ key = key_and_value[0]
183
+ value = key_and_value[1]
184
+ attribute_result[key.to_s] = value.to_s
185
+ attribute_result
186
+ end
187
+ inner_result
188
+ end
189
+ result
190
+ end
191
+ end
192
+
193
+ def self.write_database_to_file(path)
194
+ File.open(path,'w'){|f| f.write(PhotoFolder::hash_from_database.to_json) }
195
+ end
196
+
197
+ def self.source_directory_location
198
+ @source_directory_location
199
+ end
200
+
201
+ def self.generate(source_directory_location,database_location,json_location,include_nsfw = false,exif_tool_location = 'exiftool')
202
+ Photo.send(:with_scope, :find => {:conditions => include_nsfw ? "nsfw = 1 OR nsfw = 0" : "nsfw = 0"}) do
203
+
204
+ @source_directory_location = source_directory_location
205
+
206
+ begin
207
+ MiniExiftool.configure(exif_tool_location)
208
+ rescue
209
+ puts "WARNING: Could not configure MiniExifTool, EXIF data will not be parsed."
210
+ end
211
+
212
+ puts "PhotoFolder Database Generator"
213
+ puts "=============================="
214
+
215
+ PhotoFolder::setup_database(database_location)
216
+ photos_list = PhotoFolder::photos_list
217
+ puts "Scanning #{photos_list.length} photos in #{source_directory_location}"
218
+
219
+ photos_to_add, photos_to_update, photos_to_remove = PhotoFolder::scan_photos
220
+
221
+ Photo.transaction do
222
+ if photos_to_add.length > 0
223
+ photos_to_add.each do |path|
224
+ puts "Adding photo: #{path}"
225
+ Photo.create :path => path
226
+ end
227
+ else
228
+ puts "No photos to add."
229
+ end
230
+
231
+ if photos_to_update.length > 0
232
+ photos_to_update.each do |path|
233
+ puts "Updating photo: #{path}"
234
+ photo = Photo.find_by_path(path)
235
+ photo.update_meta if photo
236
+ end
237
+ else
238
+ puts "No photos to update."
239
+ end
240
+
241
+ if photos_to_remove.length > 0
242
+ photos_to_remove.each do |path|
243
+ puts "Removing photo: #{path}"
244
+ Photo.find_all_by_path(path).each(&:destroy)
245
+ end
246
+ else
247
+ puts "No photos to remove."
248
+ end
249
+ end
250
+
251
+ PhotoFolder::flush_cached_path_information
252
+ collections_to_add, collections_to_remove = PhotoFolder::scan_photo_collections
253
+
254
+ PhotoCollection.transaction do
255
+ if collections_to_add.length > 0
256
+ collections_to_add.sort_by{|path| path.split('/').pop}.each do |path|
257
+ puts "Adding collection: #{path}"
258
+ PhotoCollection.create :path => path
259
+ end
260
+ else
261
+ puts "No photo collections to add."
262
+ end
263
+
264
+ if collections_to_remove.length > 0
265
+ collections_to_remove.each do |path|
266
+ puts "Removing collection: #{path}"
267
+ PhotoCollection.find_by_path(path).destroy if PhotoCollection.find_by_path(path)
268
+ end
269
+ else
270
+ puts "No photo collections to remove."
271
+ end
272
+
273
+ puts "Ensuring correct tree structure of photo collections."
274
+ PhotoCollection.find(:all).each(&:set_parent_id)
275
+
276
+ puts "Ensuring correct position of photos and photo collections."
277
+ if PhotoCollection.positions
278
+ puts "Setting positions for root collections from #{PhotoCollection.positions_file_path}"
279
+ end
280
+ PhotoCollection.set_correct_positions(PhotoCollection.find_all_by_parent_id(0))
281
+ PhotoCollection.find(:all).each do |photo_collection|
282
+ if photo_collection.positions
283
+ puts "Setting positions for #{photo_collection.name} from #{photo_collection.positions_file_path}"
284
+ photo_collection.set_correct_positions
285
+ end
286
+ if photo_collection.set_meta
287
+ puts "Setting meta data for #{photo_collection.name} from #{photo_collection.meta_file_path}"
288
+ end
289
+ end
290
+ end
291
+
292
+ puts "Writing database to #{json_location}"
293
+ PhotoFolder::write_database_to_file(json_location)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "photo_folder"
3
+ s.version = "1.0"
4
+ s.date = "2010-05-11"
5
+ s.summary = "JavaScript gallery"
6
+ s.email = "ryan@syntacticx.com"
7
+ s.homepage = "http://photofolder.org/"
8
+ s.description = "JavaScript gallery"
9
+ s.has_rdoc = false
10
+ s.authors = ["Ryan Johnson"]
11
+ s.files = [
12
+ "README.markdown",
13
+ "photo_folder.gemspec"] +
14
+ Dir['lib/**/*'] +
15
+ Dir['vendor/**/*']
16
+ s.autorequire = "lib/photo_folder.rb"
17
+ s.add_dependency("sqlite3-ruby", ["> 0.0.0"])
18
+ s.add_dependency("activerecord", ["> 0.0.0"])
19
+ end
@@ -0,0 +1,257 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module List #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ def acts_as_list(options = {})
34
+ configuration = { :column => "position", :scope => "1 = 1" }
35
+ configuration.update(options) if options.is_a?(Hash)
36
+
37
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38
+
39
+ if configuration[:scope].is_a?(Symbol)
40
+ scope_condition_method = %(
41
+ def scope_condition
42
+ if #{configuration[:scope].to_s}.nil?
43
+ "#{configuration[:scope].to_s} IS NULL"
44
+ else
45
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46
+ end
47
+ end
48
+ )
49
+ else
50
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51
+ end
52
+
53
+ class_eval <<-EOV
54
+ include ActiveRecord::Acts::List::InstanceMethods
55
+
56
+ def acts_as_list_class
57
+ ::#{self.name}
58
+ end
59
+
60
+ def position_column
61
+ '#{configuration[:column]}'
62
+ end
63
+
64
+ #{scope_condition_method}
65
+
66
+ before_destroy :remove_from_list
67
+ before_create :add_to_list_bottom
68
+ EOV
69
+ end
70
+ end
71
+
72
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75
+ # the first in the list of all chapters.
76
+ module InstanceMethods
77
+ # Insert the item at the given position (defaults to the top position of 1).
78
+ def insert_at(position = 1)
79
+ insert_at_position(position)
80
+ end
81
+
82
+ # Swap positions with the next lower item, if one exists.
83
+ def move_lower
84
+ return unless lower_item
85
+
86
+ acts_as_list_class.transaction do
87
+ lower_item.decrement_position
88
+ increment_position
89
+ end
90
+ end
91
+
92
+ # Swap positions with the next higher item, if one exists.
93
+ def move_higher
94
+ return unless higher_item
95
+
96
+ acts_as_list_class.transaction do
97
+ higher_item.increment_position
98
+ decrement_position
99
+ end
100
+ end
101
+
102
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
103
+ # position adjusted accordingly.
104
+ def move_to_bottom
105
+ return unless in_list?
106
+ acts_as_list_class.transaction do
107
+ decrement_positions_on_lower_items
108
+ assume_bottom_position
109
+ end
110
+ end
111
+
112
+ # Move to the top of the list. If the item is already in the list, the items above it have their
113
+ # position adjusted accordingly.
114
+ def move_to_top
115
+ return unless in_list?
116
+ acts_as_list_class.transaction do
117
+ increment_positions_on_higher_items
118
+ assume_top_position
119
+ end
120
+ end
121
+
122
+ # Removes the item from the list.
123
+ def remove_from_list
124
+ if in_list?
125
+ decrement_positions_on_lower_items
126
+ update_attribute position_column, nil
127
+ end
128
+ end
129
+
130
+ # Increase the position of this item without adjusting the rest of the list.
131
+ def increment_position
132
+ return unless in_list?
133
+ update_attribute position_column, self.send(position_column).to_i + 1
134
+ end
135
+
136
+ # Decrease the position of this item without adjusting the rest of the list.
137
+ def decrement_position
138
+ return unless in_list?
139
+ update_attribute position_column, self.send(position_column).to_i - 1
140
+ end
141
+
142
+ # Return +true+ if this object is the first in the list.
143
+ def first?
144
+ return false unless in_list?
145
+ self.send(position_column) == 1
146
+ end
147
+
148
+ # Return +true+ if this object is the last in the list.
149
+ def last?
150
+ return false unless in_list?
151
+ self.send(position_column) == bottom_position_in_list
152
+ end
153
+
154
+ # Return the next higher item in the list.
155
+ def higher_item
156
+ return nil unless in_list?
157
+ acts_as_list_class.find(:first, :conditions =>
158
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159
+ )
160
+ end
161
+
162
+ # Return the next lower item in the list.
163
+ def lower_item
164
+ return nil unless in_list?
165
+ acts_as_list_class.find(:first, :conditions =>
166
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167
+ )
168
+ end
169
+
170
+ # Test if this record is in a list
171
+ def in_list?
172
+ !send(position_column).nil?
173
+ end
174
+
175
+ private
176
+ def add_to_list_top
177
+ increment_positions_on_all_items
178
+ end
179
+
180
+ def add_to_list_bottom
181
+ self[position_column] = bottom_position_in_list.to_i + 1
182
+ end
183
+
184
+ # Overwrite this method to define the scope of the list changes
185
+ def scope_condition() "1" end
186
+
187
+ # Returns the bottom position number in the list.
188
+ # bottom_position_in_list # => 2
189
+ def bottom_position_in_list(except = nil)
190
+ item = bottom_item(except)
191
+ item ? item.send(position_column) : 0
192
+ end
193
+
194
+ # Returns the bottom item
195
+ def bottom_item(except = nil)
196
+ conditions = scope_condition
197
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199
+ end
200
+
201
+ # Forces item to assume the bottom position in the list.
202
+ def assume_bottom_position
203
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204
+ end
205
+
206
+ # Forces item to assume the top position in the list.
207
+ def assume_top_position
208
+ update_attribute(position_column, 1)
209
+ end
210
+
211
+ # This has the effect of moving all the higher items up one.
212
+ def decrement_positions_on_higher_items(position)
213
+ acts_as_list_class.update_all(
214
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215
+ )
216
+ end
217
+
218
+ # This has the effect of moving all the lower items up one.
219
+ def decrement_positions_on_lower_items
220
+ return unless in_list?
221
+ acts_as_list_class.update_all(
222
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223
+ )
224
+ end
225
+
226
+ # This has the effect of moving all the higher items down one.
227
+ def increment_positions_on_higher_items
228
+ return unless in_list?
229
+ acts_as_list_class.update_all(
230
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231
+ )
232
+ end
233
+
234
+ # This has the effect of moving all the lower items down one.
235
+ def increment_positions_on_lower_items(position)
236
+ acts_as_list_class.update_all(
237
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238
+ )
239
+ end
240
+
241
+ # Increments position (<tt>position_column</tt>) of all items in the list.
242
+ def increment_positions_on_all_items
243
+ acts_as_list_class.update_all(
244
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
245
+ )
246
+ end
247
+
248
+ def insert_at_position(position)
249
+ remove_from_list
250
+ increment_positions_on_lower_items(position)
251
+ self.update_attribute(position_column, position)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
@@ -0,0 +1,97 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Tree
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
9
+ # association. This requires that you have a foreign key column, which by default is called +parent_id+.
10
+ #
11
+ # class Category < ActiveRecord::Base
12
+ # acts_as_tree :order => "name"
13
+ # end
14
+ #
15
+ # Example:
16
+ # root
17
+ # \_ child1
18
+ # \_ subchild1
19
+ # \_ subchild2
20
+ #
21
+ # root = Category.create("name" => "root")
22
+ # child1 = root.children.create("name" => "child1")
23
+ # subchild1 = child1.children.create("name" => "subchild1")
24
+ #
25
+ # root.parent # => nil
26
+ # child1.parent # => root
27
+ # root.children # => [child1]
28
+ # root.children.first.children.first # => subchild1
29
+ #
30
+ # In addition to the parent and children associations, the following instance methods are added to the class
31
+ # after calling <tt>acts_as_tree</tt>:
32
+ # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
33
+ # * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
34
+ # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
35
+ # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
36
+ module ClassMethods
37
+ # Configuration options are:
38
+ #
39
+ # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
40
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
41
+ # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
42
+ def acts_as_tree(options = {})
43
+ configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
44
+ configuration.update(options) if options.is_a?(Hash)
45
+
46
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
47
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
48
+
49
+ class_eval <<-EOV
50
+ include ActiveRecord::Acts::Tree::InstanceMethods
51
+
52
+ def self.roots
53
+ find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
54
+ end
55
+
56
+ def self.root
57
+ find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
58
+ end
59
+ EOV
60
+ end
61
+ end
62
+
63
+ module InstanceMethods
64
+ # Returns list of ancestors, starting from parent until root.
65
+ #
66
+ # subchild1.ancestors # => [child1, root]
67
+ def ancestors
68
+ node, nodes = self, []
69
+ nodes << node = node.parent while node.parent
70
+ nodes
71
+ end
72
+
73
+ # Returns the root node of the tree.
74
+ def root
75
+ node = self
76
+ node = node.parent while node.parent
77
+ node
78
+ end
79
+
80
+ # Returns all siblings of the current node.
81
+ #
82
+ # subchild1.siblings # => [subchild2]
83
+ def siblings
84
+ self_and_siblings - [self]
85
+ end
86
+
87
+ # Returns all siblings and a reference to the current node.
88
+ #
89
+ # subchild1.self_and_siblings # => [subchild1, subchild2]
90
+ def self_and_siblings
91
+ parent ? parent.children : self.class.roots
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
@@ -0,0 +1,416 @@
1
+ class MiniExiftool
2
+
3
+ def self.configure(exif_tool_location)
4
+ MiniExiftool.cmd = exif_tool_location
5
+ if Float(exiftool_version) < 7.41
6
+ @@separator = ', '
7
+ @@sep_op = ''
8
+ else
9
+ @@separator = '@@'
10
+ @@sep_op = '-sep @@'
11
+ end
12
+ end
13
+
14
+ # Name of the Exiftool command-line application
15
+ @@cmd = nil
16
+ cattr_accessor :cmd
17
+
18
+ # Hash of the standard options used when call MiniExiftool.new
19
+ @@opts = { :numerical => false, :composite => true, :convert_encoding => false, :timestamps => Time }
20
+
21
+ attr_reader :filename
22
+ attr_accessor :numerical, :composite, :convert_encoding, :errors, :timestamps
23
+
24
+ VERSION = '1.0.1'
25
+
26
+ # +opts+ support at the moment
27
+ # * <code>:numerical</code> for numerical values, default is +false+
28
+ # * <code>:composite</code> for including composite tags while loading,
29
+ # default is +true+
30
+ # * <code>:convert_encoding</code> convert encoding (See -L-option of
31
+ # the exiftool command-line application, default is +false+
32
+ # * <code>:timestamps</code> generating DateTime objects instead of
33
+ # Time objects if set to <code>DateTime</code>, default is +Time+
34
+ #
35
+ # <b>ATTENTION:</b> Time objects are created using <code>Time.local</code>
36
+ # therefore they use <em>your local timezone</em>, DateTime objects instead
37
+ # are created <em>without timezone</em>!
38
+ def initialize filename=nil, opts={}
39
+ opts = @@opts.merge opts
40
+ @numerical = opts[:numerical]
41
+ @composite = opts[:composite]
42
+ @convert_encoding = opts[:convert_encoding]
43
+ @timestamps = opts[:timestamps]
44
+ @values = TagHash.new
45
+ @tag_names = TagHash.new
46
+ @changed_values = TagHash.new
47
+ @errors = TagHash.new
48
+ load filename unless filename.nil?
49
+ end
50
+
51
+ def initialize_from_hash hash # :nodoc:
52
+ hash.each_pair do |tag,value|
53
+ set_value tag, value
54
+ end
55
+ set_attributes_by_heuristic
56
+ self
57
+ end
58
+
59
+ # Load the tags of filename.
60
+ def load filename
61
+ unless filename && File.exist?(filename)
62
+ raise MiniExiftool::Error.new("File '#{filename}' does not exist.")
63
+ end
64
+ if File.directory?(filename)
65
+ raise MiniExiftool::Error.new("'#{filename}' is a directory.")
66
+ end
67
+ @filename = filename
68
+ @values.clear
69
+ @tag_names.clear
70
+ @changed_values.clear
71
+ opt_params = ''
72
+ opt_params << (@numerical ? '-n ' : '')
73
+ opt_params << (@composite ? '' : '-e ')
74
+ opt_params << (@convert_encoding ? '-L ' : '')
75
+ cmd = %Q(#@@cmd -q -q -s -t #{opt_params} #{@@sep_op} "#{filename}")
76
+ if run(cmd)
77
+ parse_output
78
+ else
79
+ raise MiniExiftool::Error.new(@error_text)
80
+ end
81
+ self
82
+ end
83
+
84
+ # Reload the tags of an already readed file.
85
+ def reload
86
+ load @filename
87
+ end
88
+
89
+ # Returns the value of a tag.
90
+ def [] tag
91
+ @changed_values[tag] || @values[tag]
92
+ end
93
+
94
+ # Set the value of a tag.
95
+ def []=(tag, val)
96
+ @changed_values[tag] = val
97
+ end
98
+
99
+ # Returns true if any tag value is changed or if the value of a
100
+ # given tag is changed.
101
+ def changed? tag=false
102
+ if tag
103
+ @changed_values.include? tag
104
+ else
105
+ !@changed_values.empty?
106
+ end
107
+ end
108
+
109
+ # Revert all changes or the change of a given tag.
110
+ def revert tag=nil
111
+ if tag
112
+ val = @changed_values.delete(tag)
113
+ res = val != nil
114
+ else
115
+ res = @changed_values.size > 0
116
+ @changed_values.clear
117
+ end
118
+ res
119
+ end
120
+
121
+ # Returns an array of the tags (original tag names) of the readed file.
122
+ def tags
123
+ @values.keys.map { |key| @tag_names[key] }
124
+ end
125
+
126
+ # Returns an array of all changed tags.
127
+ def changed_tags
128
+ @changed_values.keys.map { |key| MiniExiftool.original_tag(key) }
129
+ end
130
+
131
+ # Save the changes to the file.
132
+ def save
133
+ return false if @changed_values.empty?
134
+ @errors.clear
135
+ temp_file = Tempfile.new('mini_exiftool')
136
+ temp_file.close
137
+ temp_filename = temp_file.path
138
+ FileUtils.cp filename, temp_filename
139
+ all_ok = true
140
+ @changed_values.each do |tag, val|
141
+ original_tag = MiniExiftool.original_tag(tag)
142
+ arr_val = val.kind_of?(Array) ? val : [val]
143
+ arr_val.map! {|e| convert e}
144
+ tag_params = ''
145
+ arr_val.each do |v|
146
+ tag_params << %Q(-#{original_tag}="#{v}" )
147
+ end
148
+ opt_params = ''
149
+ opt_params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
150
+ opt_params << (@convert_encoding ? '-L ' : '')
151
+ cmd = %Q(#@@cmd -q -P -overwrite_original #{opt_params} #{tag_params} "#{temp_filename}")
152
+ result = run(cmd)
153
+ unless result
154
+ all_ok = false
155
+ @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp
156
+ end
157
+ end
158
+ if all_ok
159
+ FileUtils.cp temp_filename, filename
160
+ reload
161
+ end
162
+ temp_file.delete
163
+ all_ok
164
+ end
165
+
166
+ # Returns a hash of the original loaded values of the MiniExiftool
167
+ # instance.
168
+ def to_hash
169
+ result = {}
170
+ @values.each do |k,v|
171
+ result[@tag_names[k]] = v
172
+ end
173
+ result
174
+ end
175
+
176
+ # Returns a YAML representation of the original loaded values of the
177
+ # MiniExiftool instance.
178
+ def to_yaml
179
+ to_hash.to_yaml
180
+ end
181
+
182
+ # Create a MiniExiftool instance from a hash
183
+ def self.from_hash hash
184
+ instance = MiniExiftool.new
185
+ instance.initialize_from_hash hash
186
+ instance
187
+ end
188
+
189
+ # Create a MiniExiftool instance from YAML data created with
190
+ # MiniExiftool#to_yaml
191
+ def self.from_yaml yaml
192
+ MiniExiftool.from_hash YAML.load(yaml)
193
+ end
194
+
195
+ # Returns the command name of the called Exiftool application.
196
+ def self.command
197
+ @@cmd
198
+ end
199
+
200
+ # Setting the command name of the called Exiftool application.
201
+ def self.command= cmd
202
+ @@cmd = cmd
203
+ end
204
+
205
+ # Returns the options hash.
206
+ def self.opts
207
+ @@opts
208
+ end
209
+
210
+ # Returns a set of all known tags of Exiftool.
211
+ def self.all_tags
212
+ unless defined? @@all_tags
213
+ @@all_tags = pstore_get :all_tags
214
+ end
215
+ @@all_tags
216
+ end
217
+
218
+ # Returns a set of all possible writable tags of Exiftool.
219
+ def self.writable_tags
220
+ unless defined? @@writable_tags
221
+ @@writable_tags = pstore_get :writable_tags
222
+ end
223
+ @@writable_tags
224
+ end
225
+
226
+ # Returns the original Exiftool name of the given tag
227
+ def self.original_tag tag
228
+ unless defined? @@all_tags_map
229
+ @@all_tags_map = pstore_get :all_tags_map
230
+ end
231
+ @@all_tags_map[tag]
232
+ end
233
+
234
+ # Returns the version of the Exiftool command-line application.
235
+ def self.exiftool_version
236
+ output = `#{MiniExiftool.command} -ver 2>&1`
237
+ unless $?.exitstatus == 0
238
+ raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found")
239
+ end
240
+ output.chomp!
241
+ end
242
+
243
+ def self.unify tag
244
+ tag.gsub(/[-_]/,'').downcase
245
+ end
246
+
247
+ # Exception class
248
+ class MiniExiftool::Error < StandardError; end
249
+
250
+ ############################################################################
251
+ private
252
+ ############################################################################
253
+
254
+ @@error_file = Tempfile.new 'errors'
255
+ @@error_file.close
256
+
257
+ def run cmd
258
+ if $DEBUG
259
+ $stderr.puts cmd
260
+ end
261
+ @output = `#{cmd} 2>#{@@error_file.path}`
262
+ @status = $?
263
+ unless @status.exitstatus == 0
264
+ @error_text = File.readlines(@@error_file.path).join
265
+ return false
266
+ else
267
+ @error_text = ''
268
+ return true
269
+ end
270
+ end
271
+
272
+ def convert val
273
+ case val
274
+ when Time
275
+ val = val.strftime('%Y:%m:%d %H:%M:%S')
276
+ end
277
+ val
278
+ end
279
+
280
+ def method_missing symbol, *args
281
+ tag_name = symbol.id2name
282
+ if tag_name.sub!(/=$/, '')
283
+ self[tag_name] = args.first
284
+ else
285
+ self[tag_name]
286
+ end
287
+ end
288
+
289
+ def parse_output
290
+ @output.each_line do |line|
291
+ tag, value = parse_line line
292
+ set_value tag, value
293
+ end
294
+ end
295
+
296
+ def parse_line line
297
+ if line =~ /^([^\t]+)\t(.*)$/
298
+ tag, value = $1, $2
299
+ case value
300
+ when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d$/
301
+ arr = value.split /[: ]/
302
+ arr.map! {|elem| elem.to_i}
303
+ begin
304
+ if @timestamps == Time
305
+ value = Time.local *arr
306
+ elsif @timestamps == DateTime
307
+ value = DateTime.strptime(value,'%Y:%m:%d %H:%M:%S')
308
+ else
309
+ raise MiniExiftool::Error.new("Value #@timestamps not allowed for option timestamps.")
310
+ end
311
+ rescue ArgumentError
312
+ value = false
313
+ end
314
+ when /^\d+\.\d+$/
315
+ value = value.to_f
316
+ when /^0+[1-9]+$/
317
+ # nothing => String
318
+ when /^-?\d+$/
319
+ value = value.to_i
320
+ when /^[\d ]+$/
321
+ # nothing => String
322
+ when /#{@@separator}/
323
+ value = value.split @@separator
324
+ end
325
+ else
326
+ raise MiniExiftool::Error.new("Malformed line #{line.inspect} of exiftool output.")
327
+ end
328
+ unless value.respond_to?('to_a')
329
+ class << value
330
+ def to_a
331
+ [self]
332
+ end
333
+ end
334
+ end
335
+ return [tag, value]
336
+ end
337
+
338
+ def set_value tag, value
339
+ @tag_names[tag] = tag
340
+ @values[tag] = value
341
+ end
342
+
343
+ def set_attributes_by_heuristic
344
+ self.composite = tags.include?('ImageSize') ? true : false
345
+ self.numerical = self.file_size.kind_of?(Integer) ? true : false
346
+ # TODO: Is there a heuristic to determine @convert_encoding?
347
+ self.timestamps = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time
348
+ end
349
+
350
+ def temp_filename
351
+ unless @temp_filename
352
+ temp_file = Tempfile.new('mini-exiftool')
353
+ temp_file.close
354
+ FileUtils.cp(@filename, temp_file.path)
355
+ @temp_filename = temp_file.path
356
+ end
357
+ @temp_filename
358
+ end
359
+
360
+ def self.pstore_get attribute
361
+ load_or_create_pstore unless defined? @@pstore
362
+ result = nil
363
+ @@pstore.transaction(true) do |ps|
364
+ result = ps[attribute]
365
+ end
366
+ result
367
+ end
368
+
369
+ def self.load_or_create_pstore
370
+ # This will hopefully work on *NIX and Windows systems
371
+ home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE']
372
+ subdir = RUBY_PLATFORM =~ /win/i ? '_mini_exiftool' : '.mini_exiftool'
373
+ FileUtils.mkdir_p(File.join(home, subdir))
374
+ filename = File.join(home, subdir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore')
375
+ @@pstore = PStore.new filename
376
+ if !File.exist?(filename) || File.size(filename) == 0
377
+ @@pstore.transaction do |ps|
378
+ ps[:all_tags] = all_tags = determine_tags('list')
379
+ ps[:writable_tags] = determine_tags('listw')
380
+ map = {}
381
+ all_tags.each { |k| map[unify(k)] = k }
382
+ ps[:all_tags_map] = map
383
+ end
384
+ end
385
+ end
386
+
387
+ def self.determine_tags arg
388
+ output = `#{@@cmd} -#{arg}`
389
+ lines = output.split /\n/
390
+ tags = Set.new
391
+ lines.each do |line|
392
+ next unless line =~ /^\s/
393
+ tags |= line.chomp.split
394
+ end
395
+ tags
396
+ end
397
+
398
+
399
+ # Hash with indifferent access:
400
+ # DateTimeOriginal == datetimeoriginal == date_time_original
401
+ class TagHash < Hash # :nodoc:
402
+ def[] k
403
+ super(unify(k))
404
+ end
405
+ def []= k, v
406
+ super(unify(k), v)
407
+ end
408
+ def delete k
409
+ super(unify(k))
410
+ end
411
+
412
+ def unify tag
413
+ MiniExiftool.unify tag
414
+ end
415
+ end
416
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: photo_folder
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Johnson
8
+ autorequire: lib/photo_folder.rb
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-05-11 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sqlite3-ruby
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.0
34
+ version:
35
+ description: JavaScript gallery
36
+ email: ryan@syntacticx.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - README.markdown
45
+ - photo_folder.gemspec
46
+ - lib/models/photo.rb
47
+ - lib/models/photo_collection.rb
48
+ - lib/models/photo_tag.rb
49
+ - lib/models/photo_tagging.rb
50
+ - lib/photo_folder.rb
51
+ - vendor/acts_as_list.rb
52
+ - vendor/acts_as_tree.rb
53
+ - vendor/mini_exiftool.rb
54
+ has_rdoc: true
55
+ homepage: http://photofolder.org/
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.5
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: JavaScript gallery
82
+ test_files: []
83
+