photo_folder 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+