piccle 0.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/NOTES.md +69 -0
- data/README.md +175 -0
- data/Rakefile +8 -0
- data/agpl-3.0.md +660 -0
- data/assets/css/default.css +397 -0
- data/assets/css/normalize.css +427 -0
- data/assets/icons/android-chrome-192x192.png +0 -0
- data/assets/icons/android-chrome-512x512.png +0 -0
- data/assets/icons/apple-touch-icon.png +0 -0
- data/assets/icons/favicon-16x16.png +0 -0
- data/assets/icons/favicon-32x32.png +0 -0
- data/assets/icons/favicon.ico +0 -0
- data/bin/console +14 -0
- data/bin/piccle +355 -0
- data/bin/setup +8 -0
- data/db/migrations/001_create_photos.rb +15 -0
- data/db/migrations/002_update_photos.rb +14 -0
- data/db/migrations/003_create_keywords_and_join_table.rb +14 -0
- data/db/migrations/004_add_focal_length.rb +7 -0
- data/db/migrations/005_create_locations.rb +20 -0
- data/js-renderer/handlebars.min-v4.7.6.js +29 -0
- data/js-renderer/renderer.js +93 -0
- data/lib/piccle.rb +52 -0
- data/lib/piccle/config.rb +136 -0
- data/lib/piccle/database.rb +33 -0
- data/lib/piccle/dstk_service.rb +64 -0
- data/lib/piccle/extractor.rb +128 -0
- data/lib/piccle/js_renderer.rb +37 -0
- data/lib/piccle/models/keyword.rb +6 -0
- data/lib/piccle/models/location.rb +11 -0
- data/lib/piccle/models/photo.rb +211 -0
- data/lib/piccle/parser.rb +230 -0
- data/lib/piccle/quilt_generator.rb +30 -0
- data/lib/piccle/renderer.rb +175 -0
- data/lib/piccle/streams.rb +2 -0
- data/lib/piccle/streams/base_stream.rb +56 -0
- data/lib/piccle/streams/camera_stream.rb +35 -0
- data/lib/piccle/streams/date_stream.rb +95 -0
- data/lib/piccle/streams/event_stream.rb +73 -0
- data/lib/piccle/streams/keyword_stream.rb +24 -0
- data/lib/piccle/streams/location_stream.rb +57 -0
- data/lib/piccle/template_helpers.rb +79 -0
- data/lib/piccle/version.rb +3 -0
- data/lib/tasks/development.rake +38 -0
- data/piccle.gemspec +43 -0
- data/templates/_breadcrumbs.handlebars.slim +16 -0
- data/templates/_footer.handlebars.slim +2 -0
- data/templates/_header.handlebars.slim +36 -0
- data/templates/_navigation.handlebars.slim +16 -0
- data/templates/_substream.handlebars.slim +17 -0
- data/templates/feed.atom.slim +29 -0
- data/templates/index.html.handlebars.slim +36 -0
- data/templates/show.html.handlebars.slim +64 -0
- metadata +340 -0
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "piccle"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/piccle
ADDED
@@ -0,0 +1,355 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'handlebars'
|
5
|
+
require 'json'
|
6
|
+
require 'slim'
|
7
|
+
require 'thor'
|
8
|
+
require 'piccle'
|
9
|
+
|
10
|
+
class CLI < Thor
|
11
|
+
default_task :generate
|
12
|
+
class_option :"image-dir", desc: "Input image directory. Defaults to $CWD/images.", aliases: "-i"
|
13
|
+
class_option :database, desc: "The location of the database to use. Defaults to $CWD/piccle_data.db.", aliases: "-d"
|
14
|
+
class_option :config, desc: "Config file to use. Defaults to $CWD/piccle.config.yaml, then ~/.piccle.config.yaml.", aliases: "-c"
|
15
|
+
class_option :debug, desc: "Enable debug mode.", type: :boolean, default: false
|
16
|
+
|
17
|
+
# Prints a banner describing Piccle. qv. https://github.com/erikhuda/thor/issues/612
|
18
|
+
def help(*)
|
19
|
+
print_wrapped <<~INTRO
|
20
|
+
Piccle #{Piccle::VERSION}: a static website generator for photographers.
|
21
|
+
|
22
|
+
Piccle reads the metadata from your photos and uses it to generate a gallery that lets people explore
|
23
|
+
your photos by date, keyword, location, etc. Run "piccle help generate" for more details, or see
|
24
|
+
usage instructions at https://piccle.alexpounds.com/.
|
25
|
+
INTRO
|
26
|
+
puts
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "generate", "Generates a web photo gallery based on image metadata."
|
31
|
+
option :"output-dir", desc: "Output directory. Defaults to $CWD/generated.", aliases: "-o"
|
32
|
+
option :events, desc: "The location of the events file to use, if any. Defaults to $CWD/events.yaml", aliases: "-e"
|
33
|
+
option :"author-name", desc: "Author name.", aliases: "-n"
|
34
|
+
option :url, desc: "The URL where you'll deploy your gallery, if any. Used to generate Atom feeds and OpenGraph tags.", aliases: "-u"
|
35
|
+
option :"ruby-renderer", desc: "Render templates with a Ruby codepath, rather than a JavaScript helper app. You don't need node in your path, but it is 10x slower.", type: :boolean, default: false
|
36
|
+
def generate
|
37
|
+
if options.key?("config") && !File.exist?(options["config"])
|
38
|
+
puts "Specified config file #{options["config"]} not found."
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
Piccle.config = piccle_config(options)
|
43
|
+
report_options
|
44
|
+
check_image_dir_exists
|
45
|
+
|
46
|
+
update_db
|
47
|
+
generate_everything
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "geocode", "Retrieve locations from photos, and convert lat/longs to named locations."
|
51
|
+
def geocode
|
52
|
+
Piccle.config = piccle_config(options)
|
53
|
+
report_image_and_config_options
|
54
|
+
check_image_dir_exists
|
55
|
+
|
56
|
+
Piccle::Photo.where(path: Piccle.config.images_dir).each do |photo|
|
57
|
+
if File.exist?(File.join(Piccle.config.images_dir, photo.file_name))
|
58
|
+
print "Updating location data for #{photo.file_name}... "
|
59
|
+
|
60
|
+
# Is this photo fully geocoded? That is, does it have lat/long/city/state/country?
|
61
|
+
if photo.geocoded?
|
62
|
+
puts " Already geocoded, no changes made."
|
63
|
+
save_location_data(photo)
|
64
|
+
|
65
|
+
# Does it have just a lat/long? Either retrieve a cached location record or look it up.
|
66
|
+
elsif photo.latitude && photo.longitude
|
67
|
+
puts "\n Photo has lat/long, looking for place data... "
|
68
|
+
dstk_service = Piccle::DstkService.new
|
69
|
+
if location = dstk_service.location_for(photo)
|
70
|
+
if photo.update(city: location.city, state: location.state, country: location.country)
|
71
|
+
puts " Done."
|
72
|
+
else
|
73
|
+
puts " Couldn't save data: #{photo.errors.inspect}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Maybe it's got city/state/country in the DB already.
|
78
|
+
else
|
79
|
+
places = [photo.city, photo.state, photo.country].compact
|
80
|
+
if places.any?
|
81
|
+
puts " Photo has metadata labels for #{places.join(", ")}, no changes made."
|
82
|
+
else
|
83
|
+
puts " No geo information in this photo's metadata, no changes made."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Merge our supplied options with a couple of required details, and return a config object.
|
93
|
+
def piccle_config(options)
|
94
|
+
Piccle::Config.new(options.merge("working_directory" => Dir.pwd, "home_directory" => Dir.home))
|
95
|
+
end
|
96
|
+
|
97
|
+
# How are we going to generate this gallery - based on which options?
|
98
|
+
def report_options
|
99
|
+
report_image_and_config_options
|
100
|
+
option_message("Writing gallery to #{Piccle.config.output_dir}", "output-dir")
|
101
|
+
option_message("Photos will be credited to #{Piccle.config.author_name}", "author-name")
|
102
|
+
|
103
|
+
if Piccle.config.using_default?("events") && !File.exist?(Piccle.config.events_file)
|
104
|
+
puts "No events file found."
|
105
|
+
elsif File.exist?(Piccle.config.events_file)
|
106
|
+
option_message("Events read from #{Piccle.config.events_file}", "events")
|
107
|
+
else
|
108
|
+
option_message("⚠️ Events file #{Piccle.config.events_file}", "events")
|
109
|
+
end
|
110
|
+
|
111
|
+
puts "⚠️ Not generating an Atom feed, because URL is unset." unless Piccle.config.atom?
|
112
|
+
puts "⚠️ Not generating OpenGraph tags, because URL is unset." unless Piccle.config.open_graph?
|
113
|
+
puts ""
|
114
|
+
end
|
115
|
+
|
116
|
+
# Geocoding has fewer relevant options, so we can output this little summary instead.
|
117
|
+
def report_image_and_config_options
|
118
|
+
option_message("Reading images from #{Piccle.config.images_dir}", "image-dir")
|
119
|
+
|
120
|
+
if Piccle.config.database_exists?
|
121
|
+
option_message("Using existing database #{Piccle.config.database_file}", "database")
|
122
|
+
else
|
123
|
+
option_message("Creating new database #{Piccle.config.database_file}", "database")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Given a message and a parameter name, generate one of our standard report strings.
|
128
|
+
def option_message(message, config_param)
|
129
|
+
puts "#{message} (#{Piccle.config.source_for(config_param)})"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Read all the images in the images directory, and load their data into the DB.
|
133
|
+
def update_db
|
134
|
+
Dir.glob(File.join(Piccle.config.images_dir, "**")).each do |filename|
|
135
|
+
print "Examining #{filename}..."
|
136
|
+
photo = Piccle::Photo.from_file(filename)
|
137
|
+
if photo.changed_hash?
|
138
|
+
print " updating..."
|
139
|
+
photo.update_from_file
|
140
|
+
puts " done."
|
141
|
+
elsif photo.freshly_created?
|
142
|
+
puts " created."
|
143
|
+
else
|
144
|
+
puts " done."
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ensure we have some images to work with; if not, output an error and quit.
|
150
|
+
def check_image_dir_exists
|
151
|
+
unless File.exist?(Piccle.config.images_dir)
|
152
|
+
STDERR.puts "\n⚠️ The images directory, #{Piccle.config.images_dir}, does not exist. You can specify it using the -i option; run 'piccle help generate' for more info."
|
153
|
+
exit 1
|
154
|
+
end
|
155
|
+
if Dir.empty?(Piccle.config.images_dir)
|
156
|
+
STDERR.puts "\n⚠️ There are no images in #{Piccle.config.images_dir}, so we cannot continue."
|
157
|
+
exit 1
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Generates an entire site. Atom feeds, HTML templates, smaller images, JSON data, copied assets, the whole enchilada.
|
162
|
+
def generate_everything
|
163
|
+
start_time = Time.now
|
164
|
+
puts "Generating website..."
|
165
|
+
|
166
|
+
parser = new_parser_with_streams
|
167
|
+
parse_photos(parser)
|
168
|
+
renderer = if Piccle.config.ruby_renderer?
|
169
|
+
Piccle::Renderer.new(parser)
|
170
|
+
else
|
171
|
+
Piccle::JsRenderer.new(parser)
|
172
|
+
end
|
173
|
+
|
174
|
+
FileUtils.mkdir_p(Piccle.config.output_dir)
|
175
|
+
generate_templates(renderer)
|
176
|
+
generate_atom_feeds(parser, renderer)
|
177
|
+
generate_html_indexes(parser, renderer)
|
178
|
+
generate_html_photos(parser, renderer)
|
179
|
+
generate_json(parser, renderer)
|
180
|
+
generate_thumbnails
|
181
|
+
generate_quilts(parser)
|
182
|
+
copy_assets
|
183
|
+
puts "Website generated in #{(Time.now - start_time)} seconds."
|
184
|
+
end
|
185
|
+
|
186
|
+
# Get a parser, with streams (metadata filters and extractors) registered.
|
187
|
+
def new_parser_with_streams
|
188
|
+
Piccle::Parser.new.tap do |p|
|
189
|
+
p.add_stream(Piccle::Streams::DateStream)
|
190
|
+
p.add_stream(Piccle::Streams::LocationStream)
|
191
|
+
p.add_stream(Piccle::Streams::EventStream)
|
192
|
+
p.add_stream(Piccle::Streams::CameraStream)
|
193
|
+
p.add_stream(Piccle::Streams::KeywordStream)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Load all the photos, and parse them all.
|
198
|
+
def parse_photos(parser)
|
199
|
+
Piccle::Photo.where(path: Piccle.config.images_dir).each do |p|
|
200
|
+
parser.parse(p)
|
201
|
+
end
|
202
|
+
parser.load_events
|
203
|
+
parser.order
|
204
|
+
end
|
205
|
+
|
206
|
+
# Given a parser object, generate some HTML index pages from the data it contains.
|
207
|
+
def generate_html_indexes(parser, renderer)
|
208
|
+
puts " ... generating HTML indexes ..."
|
209
|
+
print " ... generating main index ... "
|
210
|
+
File.write(File.join(Piccle.config.output_dir, "index.html"), renderer.render_main_index)
|
211
|
+
puts "Done."
|
212
|
+
|
213
|
+
parser.subsections.each do |subsection|
|
214
|
+
if parser.subsection_photo_hashes(subsection).any?
|
215
|
+
subdir = File.join(Piccle.config.output_dir, *subsection)
|
216
|
+
print " ... generating #{subdir} index ... "
|
217
|
+
FileUtils.mkdir_p(subdir)
|
218
|
+
File.write(File.join(subdir, "index.html"), renderer.render_index(subsection))
|
219
|
+
puts "Done."
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Given a parser object, generate Atom feeds for everything, and all substreams.
|
225
|
+
def generate_atom_feeds(parser, renderer)
|
226
|
+
if Piccle.config.atom?
|
227
|
+
puts " ... generating Atom feeds ..."
|
228
|
+
print " ... generating main Atom feed ... "
|
229
|
+
File.write(File.join(Piccle.config.output_dir, "feed.atom"), renderer.render_feed)
|
230
|
+
puts "Done."
|
231
|
+
|
232
|
+
parser.subsections.each do |subsection|
|
233
|
+
if parser.subsection_photo_hashes(subsection).any?
|
234
|
+
subdir = File.join(Piccle.config.output_dir, *subsection)
|
235
|
+
print " ... generating #{subdir} feed ... "
|
236
|
+
FileUtils.mkdir_p(subdir)
|
237
|
+
File.write(File.join(subdir, "feed.atom"), renderer.render_feed(subsection))
|
238
|
+
puts "Done."
|
239
|
+
end
|
240
|
+
end
|
241
|
+
else
|
242
|
+
puts " Not generating Atom feeds, because no home URL is set."
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Given a parser object, generate photo pages from the data it contains.
|
247
|
+
def generate_html_photos(parser, renderer)
|
248
|
+
puts " ... generating HTML photo pages ..."
|
249
|
+
parser.photo_hashes.each do |hash|
|
250
|
+
print " ... generating canonical page for #{hash}... "
|
251
|
+
File.write(File.join(Piccle.config.output_dir, "#{hash}.html"), renderer.render_photo(hash))
|
252
|
+
puts "Done."
|
253
|
+
|
254
|
+
parser.links_for(hash).each do |selector|
|
255
|
+
destination_page = File.join(Piccle.config.output_dir, *selector, "#{hash}.html")
|
256
|
+
print " ... generating stream page #{destination_page}..."
|
257
|
+
File.write(destination_page, renderer.render_photo(hash, selector))
|
258
|
+
puts "Done."
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def generate_json(parser, _renderer)
|
264
|
+
puts " ... generating JSON files..."
|
265
|
+
FileUtils.mkdir_p(File.join(Piccle.config.output_dir, "json"))
|
266
|
+
File.write(File.join(Piccle.config.output_dir, "json", "all.json"), parser.data.to_json)
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
# Stubby, hacky method that demos generating thumbnails.
|
271
|
+
def generate_thumbnails
|
272
|
+
puts " ... generating thumbnails..."
|
273
|
+
FileUtils.mkdir_p(File.join(Piccle.config.output_dir, "images", "thumbnails"))
|
274
|
+
FileUtils.mkdir_p(File.join(Piccle.config.output_dir, "images", "photos"))
|
275
|
+
|
276
|
+
Piccle::Photo.where(path: Piccle.config.images_dir).each do |photo|
|
277
|
+
print " ... generating #{photo.thumbnail_path}... "
|
278
|
+
if photo.thumbnail_exists?
|
279
|
+
puts "Already exists, skipping."
|
280
|
+
else
|
281
|
+
photo.generate_thumbnail!
|
282
|
+
puts "Done."
|
283
|
+
end
|
284
|
+
|
285
|
+
print " ... generating #{photo.full_image_path}... "
|
286
|
+
if photo.full_image_exists?
|
287
|
+
puts "Already exists, skipping."
|
288
|
+
else
|
289
|
+
photo.generate_full_image!
|
290
|
+
puts "Done."
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Generates "quilts" - stitched together images for each section, which we use in OpenGraph tags.
|
296
|
+
def generate_quilts(parser)
|
297
|
+
puts "Generating gallery quilts (preview images for sharing galleries on social media)..."
|
298
|
+
if Piccle.config.open_graph?
|
299
|
+
thumbnail_path_proc = Proc.new { |k, v| File.join(Piccle.config.output_dir, "images", "thumbnails", "#{v[:hash]}.#{v[:file_name]}") }
|
300
|
+
|
301
|
+
print " ... Creating main index quilt..."
|
302
|
+
main_thumbnails = parser.data[:photos].first(9).map(&thumbnail_path_proc)
|
303
|
+
main_quilt = Piccle::QuiltGenerator.generate_for(main_thumbnails)
|
304
|
+
main_quilt.write(File.join(Piccle.config.output_dir, "quilt.jpg"))
|
305
|
+
puts " Done."
|
306
|
+
|
307
|
+
parser.subsections.each do |subsection|
|
308
|
+
thumbnails = parser.subsection_photos(subsection).map(&thumbnail_path_proc)
|
309
|
+
if thumbnails.any?
|
310
|
+
output_path = File.join(Piccle.config.output_dir, *subsection, "quilt.jpg")
|
311
|
+
print " ... Creating gallery quilt #{output_path}..."
|
312
|
+
quilt = Piccle::QuiltGenerator.generate_for(thumbnails.first(9))
|
313
|
+
quilt.write(output_path)
|
314
|
+
puts " Done."
|
315
|
+
end
|
316
|
+
end
|
317
|
+
else
|
318
|
+
puts " Not generating gallery quilt images, because no home URL is set."
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def generate_templates(_renderer)
|
323
|
+
puts " ... generating templates..."
|
324
|
+
FileUtils.mkdir_p(File.join(Piccle.config.output_dir, "js"))
|
325
|
+
File.write(File.join(Piccle.config.output_dir, "js", "index.handlebars"), Piccle::TemplateHelpers.compile_template("index"))
|
326
|
+
File.write(File.join(Piccle.config.output_dir, "js", "show.handlebars"), Piccle::TemplateHelpers.compile_template("show"))
|
327
|
+
end
|
328
|
+
|
329
|
+
# Copy our static assets into the expected location.
|
330
|
+
def copy_assets
|
331
|
+
puts " ... copying static assets..."
|
332
|
+
puts " ... copying CSS..."
|
333
|
+
copy_asset_type("css")
|
334
|
+
puts " ... copying icons..."
|
335
|
+
copy_asset_type("icons")
|
336
|
+
end
|
337
|
+
|
338
|
+
def copy_asset_type(type)
|
339
|
+
FileUtils.mkdir_p(File.join(Piccle.config.output_dir, type))
|
340
|
+
Dir.glob("#{Piccle.config.gem_root_join("assets", type)}/**").each do |f|
|
341
|
+
FileUtils.cp(f, File.join(Piccle.config.output_dir, type, File.basename(f)))
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Take geo information from a photo, and save it to our Piccle database.
|
346
|
+
def save_location_data(photo)
|
347
|
+
unless Piccle::Location.find(latitude: photo.latitude, longitude: photo.longitude)
|
348
|
+
Piccle::Location.create(latitude: photo.latitude, longitude: photo.longitude, city: photo.city,
|
349
|
+
state: photo.state, country: photo.country)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
CLI.start(ARGV)
|
data/bin/setup
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:photos) do
|
4
|
+
primary_key :id
|
5
|
+
String :file_name, null: false, text: true
|
6
|
+
String :path, null: false, text: true
|
7
|
+
String :md5, null: false
|
8
|
+
Integer :width, null: false
|
9
|
+
Integer :height, null: false
|
10
|
+
String :camera_name, null: false, text: true
|
11
|
+
DateTime :taken_at
|
12
|
+
DateTime :created_at
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
alter_table(:photos) do
|
4
|
+
add_column :title, String, text: true
|
5
|
+
add_column :description, String, text: true
|
6
|
+
add_column :aperture, Float
|
7
|
+
add_column :shutter_speed_numerator, Integer
|
8
|
+
add_column :shutter_speed_denominator, Integer
|
9
|
+
add_column :iso, Integer
|
10
|
+
add_column :latitude, Float
|
11
|
+
add_column :longitude, Float
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:keywords) do
|
4
|
+
primary_key :id
|
5
|
+
String :name, null: false, unique: true
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table(:keywords_photos) do
|
9
|
+
Integer :photo_id, null: false
|
10
|
+
Integer :keyword_id, null: false
|
11
|
+
primary_key [:photo_id, :keyword_id], name: :keywords_photos_pk
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:locations) do
|
4
|
+
primary_key :id
|
5
|
+
Float :latitude, null: false
|
6
|
+
Float :longitude, null: false
|
7
|
+
String :city, text: true
|
8
|
+
String :state, text: true
|
9
|
+
String :country, text: true
|
10
|
+
DateTime :created_at
|
11
|
+
check { (city !~ nil) | (state !~ nil) | (country !~ nil) }
|
12
|
+
end
|
13
|
+
|
14
|
+
alter_table(:photos) do
|
15
|
+
add_column :city, String, text: true
|
16
|
+
add_column :state, String, text: true
|
17
|
+
add_column :country, String, text: true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|