dailyshare 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Tyler Kellen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # ruby-dailyshare
2
+ > 356 Project for Sinatra/Sequel
3
+
4
+ Quick and dirty extraction of code from our-365.com to facilitate making more sites like it.
5
+
6
+ Copyright (c) 2013 Tyler Kellen. See LICENSE for further details.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ Dir.glob('tasks/*.rake').each { |r| import r }
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../lib/dailyshare/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'dailyshare'
5
+ gem.version = DailyShare::VERSION
6
+ gem.summary = '356 Projects for Sinatra/Sequel'
7
+ gem.description = gem.description
8
+ gem.author = 'Tyler Kellen'
9
+ gem.email = 'tyler@sleekcode.net'
10
+ gem.homepage = 'https://github.com/tkellen/ruby-dailyshare'
11
+ gem.files = `git ls-files`.split("\n")
12
+ gem.require_paths = ['lib']
13
+ gem.add_dependency('linguistics')
14
+ gem.add_dependency('aws-s3')
15
+ gem.add_dependency('rmagick')
16
+ gem.add_dependency('exifr')
17
+ gem.add_dependency('mail')
18
+ end
@@ -0,0 +1,65 @@
1
+ require 'base64'
2
+
3
+ module DailyShare
4
+
5
+ class App < Sinatra::Base
6
+
7
+ get '/admin/?' do
8
+ protected!
9
+ redirect '/'
10
+ end
11
+
12
+ post '/upload/:date_added/:name' do
13
+ protected!
14
+ if params[:file]
15
+ params[:member_id] = Member.byName(params[:name]).id
16
+ save_photo(params,params[:file][:tempfile])
17
+ end
18
+ redirect request.referrer
19
+ end
20
+
21
+ post '/edit/:date/:name' do
22
+ protected!
23
+
24
+ member = Member.byName(params[:name])
25
+ photo = Photo.by_date_and_member(params[:date],member)
26
+ photo.update_fields(params,[:title,:description])
27
+
28
+ redirect request.referrer
29
+ end
30
+
31
+ ##
32
+ # Receive emailed photos via Postmark.
33
+ #
34
+ post '/receiver/?' do
35
+ # parse post data
36
+ data = JSON.parse(request.body.read)
37
+
38
+ # grab image data
39
+ image = data['Attachments'][0]['Content']
40
+
41
+ # check which member this is based on sender
42
+ member = Member[:email=>data['From']]
43
+
44
+ # was there a member with that email?
45
+ if !member.nil?
46
+
47
+ # get most recent photo
48
+ recent = Photo.most_recent_by_member(member)
49
+
50
+ # only continue if a photo doesn't exist for today
51
+ if !recent.nil? && recent.date_added+1 <= Date.today
52
+
53
+ # prep params for submission
54
+ params = {
55
+ :member_id => member.id,
56
+ :title => data['Subject'],
57
+ :description => data['TextBody'],
58
+ :date_added => recent.date_added+1
59
+ }
60
+ save_photo(params,StringIO.new(Base64.decode64(image)))
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,51 @@
1
+ module DailyShare
2
+
3
+ class App < Sinatra::Base
4
+
5
+ get '/' do
6
+ context = context_from_date(Date.today)
7
+ @title = "#{CONFIG['title']} - day #{context[:count]}"
8
+ slim :index, :locals => context
9
+ end
10
+
11
+ get '/logout' do
12
+ session.delete(:member_id)
13
+ session.delete(:member_name)
14
+ status 401
15
+ slim :logout
16
+ end
17
+
18
+ get '/:y/:m/:d?/?' do
19
+ begin
20
+ date = date_from_params(params)
21
+ context = context_from_date(date)
22
+ @title = "#{CONFIG['title']} - day #{context[:count]}"
23
+ rescue
24
+ pass
25
+ end
26
+
27
+ if valid_date?(date)
28
+ slim :index, :locals => context
29
+ else
30
+ redirect request.referrer
31
+ end
32
+ end
33
+
34
+ get '/:name/?' do
35
+ if (member = Member.byName(params[:name])).nil?
36
+ redirect request.referrer
37
+ end
38
+
39
+ slim :member, :locals => { :photos => member.photos }
40
+ end
41
+
42
+ get '/:name/missing/?' do
43
+ if (member = Member.byName(params[:name])).nil?
44
+ redirect request.referrer
45
+ end
46
+ slim :member_missing, :locals => { :photos => member.missingPhotos }
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,49 @@
1
+ module DailyShare
2
+ module Helpers
3
+
4
+ def date_from_params(params)
5
+ Date.parse("#{params[:y]}-#{params[:m]}-#{params[:d]}")
6
+ end
7
+
8
+ def valid_date?(date)
9
+ (date.year > 2011 || date <= Date.today)
10
+ end
11
+
12
+ def context_from_date(date)
13
+ {
14
+ :ymd => date.strftime('%Y-%m-%d'),
15
+ :date => date,
16
+ :count => (date-Date.parse('2011-12-31')).to_i.en.numwords,
17
+ :photos => Member.all_with_photo_for(date)
18
+ }
19
+ end
20
+
21
+ def prev_entry(date)
22
+ (date-1).strftime('/%Y/%m/%d/')
23
+ end
24
+
25
+ def next_entry(date)
26
+ (date+1).strftime('/%Y/%m/%d/')
27
+ end
28
+
29
+ def save_photo(params,file)
30
+ begin
31
+ photo = Photo.new
32
+ photo.set_fields(params,[:title,:description,:date_added,:member_id])
33
+ photo.save_original(file)
34
+ photo.generate_sizes
35
+ photo.save_to_s3
36
+ rescue
37
+ false
38
+ else
39
+ photo.save
40
+ send_email(
41
+ CONFIG['email']['from'],
42
+ "#{CONFIG['title']} - new submisson",
43
+ slim(:'emails/uploaded', {:locals=>{:photo=>photo},:layout=>false})
44
+ )
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,146 @@
1
+ module DailyShare
2
+ module Helpers
3
+
4
+ ##
5
+ # Generate a cache-busted URL for assets.
6
+ #
7
+ # @param [String] url
8
+ # URL to asset.
9
+ # @return [String]
10
+ # URL to asset with cache-busting string added.
11
+ #
12
+ def assetPath(url)
13
+
14
+ file = File.join(settings.public_folder,'assets',url)
15
+ if File.exists?(file)
16
+ cachebust = File.mtime(file).strftime('%s')
17
+ url = "cb#{cachebust}/#{url}"
18
+ end
19
+
20
+ if ENV['RACK_ENV'] == 'production'
21
+ CONFIG['url']['cdn']+"/assets/"+url
22
+ else
23
+ "/assets/#{url}"
24
+ end
25
+ end
26
+
27
+ ##
28
+ #
29
+ # Render a partial template using slim
30
+ #
31
+ # @param [String] template
32
+ # Path to template.
33
+ # @param [Hash] args
34
+ # Hash of options for generating a template.
35
+ # @return [String]
36
+ # Parsed template html.
37
+ #
38
+ def partial(template, *args)
39
+ template_array = template.to_s.split('/')
40
+ template = template_array[0..-2].join('/') + "/_#{template_array[-1]}"
41
+ options = args.last.is_a?(Hash) ? args.pop : {}
42
+ options.merge!(:layout => false)
43
+ locals = options[:locals] || {}
44
+ if collection = options.delete(:collection) then
45
+ collection.inject([]) do |buffer, member|
46
+ buffer << slim(:"#{template}", options.merge(:layout =>
47
+ false, :locals => { :item => member }.merge(locals)))
48
+ end.join("\n")
49
+ else
50
+ slim(:"#{template}", options)
51
+ end
52
+ end
53
+
54
+ ##
55
+ #
56
+ # Send an multi-part email using mail gem.
57
+ #
58
+ # @param [String] to
59
+ # Address to send email.
60
+ # @param [String] subject
61
+ # Subject of email to send
62
+ # @param [String] body_html
63
+ # HTML formatted body of email to send
64
+ # @param [String] from
65
+ # Address to send email from.
66
+ # @return [Boolean]
67
+ # True if mail was delivered, false if not.
68
+ #
69
+ def send_email(to,subject,body,from=CONFIG['email_from'])
70
+
71
+ # build email
72
+ mail = Mail.new do
73
+
74
+ # assign to/from/subject
75
+ to to
76
+ from from
77
+ subject subject
78
+
79
+ # cache html body
80
+ body_html = body
81
+
82
+ # strip html from sent body
83
+ body_text = body.gsub(/<\/?[^>]*>/,"")
84
+
85
+ # send text email by default
86
+ text_part do
87
+ body body_text
88
+ end
89
+
90
+ # if body contained html, send that part too
91
+ if body_html != body_text
92
+ html_part do
93
+ content_type 'text/html; charset=UTF-8'
94
+ body body
95
+ end
96
+ end
97
+ end
98
+
99
+ # send it
100
+ mail.delivery_method :sendmail
101
+ mail.deliver!
102
+ end
103
+
104
+ def protected!
105
+ unless authorized?
106
+ response['WWW-Authenticate'] = %(Basic realm="Restricted Area")
107
+ throw(:halt, [401, "Not authorized\n"])
108
+ end
109
+ end
110
+
111
+ ##
112
+ #
113
+ # Find the ordinal suffix for a number.
114
+ #
115
+ # @param [Integer] number
116
+ # A number to be ordinalize
117
+ # @return [String]
118
+ # An ordinalized number (e.g. 3 => 3rd)
119
+ #
120
+ def ordinalize(number)
121
+ if (11..13).include?(number.to_i.abs % 100)
122
+ "#{number}th"
123
+ else
124
+ case number.to_i.abs % 10
125
+ when 1; "#{number}st"
126
+ when 2; "#{number}nd"
127
+ when 3; "#{number}rd"
128
+ else "#{number}th"
129
+ end
130
+ end
131
+ end
132
+
133
+ def authorized?
134
+ return true if !session[:member_id].nil?
135
+
136
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
137
+ if @auth.provided? && @auth.basic? &&
138
+ @auth.credentials && @auth.credentials[1] == AUTH['pass'] &&
139
+ (member = Member.byName(@auth.credentials[0]))
140
+ session[:member_id] = member.id
141
+ session[:member_name] = member.name
142
+ end
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,36 @@
1
+ module DailyShare
2
+ class Member < Sequel::Model
3
+ one_to_many :photos
4
+
5
+ def self.byName(name)
6
+ self[:name=>name]
7
+ end
8
+
9
+ def self.byEmail(email)
10
+ self[:email=>email.downcase]
11
+ end
12
+
13
+ def self.all_with_photo_for(date)
14
+ select(:members__name,:p__title,:p__description,:p__date_added).
15
+ left_join(:photos.as(:p),[:p__member_id=>:members__id,:date.sql_function(:p__date_added)=>date]).
16
+ order(:members__id)
17
+ end
18
+
19
+ def missingPhotos
20
+ DB["SELECT
21
+ date('2012-01-01'::date+(interval '1 day'*s.num)) AS date
22
+ FROM
23
+ (SELECT generate_series(0,now()::date-'2012-01-01'::date) AS num) AS s
24
+ WHERE
25
+ (SELECT
26
+ true
27
+ FROM
28
+ photos
29
+ WHERE
30
+ date_added=date('2012-01-01'::date+(interval '1 day'*s.num))
31
+ AND
32
+ member_id=?) IS null",id]
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,87 @@
1
+ require "stringio"
2
+
3
+ module DailyShare
4
+ class Photo < Sequel::Model
5
+ many_to_one :member
6
+
7
+ def self.by_date_and_member(date,member)
8
+ where(:date.sql_function(:date_added)=>date,:member_id=>member.id).first
9
+ end
10
+
11
+ def self.most_recent_by_member(member)
12
+ where(:member_id=>member.id).order(:date_added.desc).first
13
+ end
14
+
15
+ def before_save
16
+ description.strip!
17
+ end
18
+
19
+ def original
20
+ "#{date_added}-#{self.member.name}.jpg"
21
+ end
22
+
23
+ def thumb
24
+ "#{date_added}-#{self.member.name}-thumb.jpg"
25
+ end
26
+
27
+ def big
28
+ "#{date_added}-#{self.member.name}-big.jpg"
29
+ end
30
+
31
+ def save_original(file)
32
+ image = File.join(CONFIG['photo_dir'],original)
33
+ File.open(image,'wb') {|f| f.write file.read }
34
+ image
35
+ end
36
+
37
+ def generate_sizes
38
+ img = Magick::Image::read(File.join(CONFIG['photo_dir'],original)).first
39
+ if img
40
+ img.resize_to_fit(240,240).write(File.join(CONFIG['photo_dir'],thumb))
41
+ img.resize_to_fit(960,680).write(File.join(CONFIG['photo_dir'],big))
42
+ img.destroy!
43
+ true
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ def sizes
50
+ [File.join(CONFIG['photo_dir'],original),
51
+ File.join(CONFIG['photo_dir'],thumb),
52
+ File.join(CONFIG['photo_dir'],big)]
53
+ end
54
+
55
+ def save_to_s3
56
+ sizes.each do |file|
57
+ AWS::S3::S3Object.store(
58
+ "/photos/#{File.basename(file)}",
59
+ open(file),
60
+ AUTH['aws']['bucket'],
61
+ {
62
+ :cache_control => 'max-age=315360000',
63
+ :access => 'public_read'
64
+ }
65
+ )
66
+ end
67
+ end
68
+
69
+ def parse_exif
70
+ img = EXIFR::JPEG.new(file)
71
+
72
+ # store fstop/focal length for comparison
73
+ fstop = img.exif[:f_number].to_f
74
+ flen = img.exif[:focal_length].to_f
75
+
76
+ {
77
+ # if no decimal is needed, leave it off
78
+ :fstop => (fstop == fstop.to_i ? fstop.to_i : fstop),
79
+ # convert exposure to string
80
+ :exposure => img.exif[:exposure_time].to_s,
81
+ # if no decimal is needed, leave it off
82
+ :focal_length => (flen == flen.to_i ? flen.to_i : flen).to_s+'mm'
83
+ }
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module DailyShare
2
+ VERSION = '0.1.0'
3
+ end
data/lib/dailyshare.rb ADDED
@@ -0,0 +1,39 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ if !defined?(Sequel) || Sequel::DATABASES.length == 0
4
+ raise "DailyShare expects a connection to Sequel before being required."
5
+ end
6
+
7
+ if !defined?(DailyShare::App)
8
+ raise "DailyShare expects a Sinatra app to exist at DailyShare::App."
9
+ end
10
+
11
+ require 'aws/s3'
12
+ require 'RMagick'
13
+ require 'exifr'
14
+ require 'mail'
15
+ require 'json'
16
+
17
+ # prep linguistics gem
18
+ Encoding.default_external = Encoding::UTF_8
19
+ Encoding.default_internal = Encoding::UTF_8
20
+ require 'linguistics'
21
+ Linguistics::use(:en)
22
+
23
+ # load helpers
24
+ require 'dailyshare/helpers/entry'
25
+ require 'dailyshare/helpers/helpers'
26
+
27
+ # load models
28
+ require 'dailyshare/models/member'
29
+ require 'dailyshare/models/photo'
30
+
31
+ # load controllers
32
+ require 'dailyshare/controllers/admin'
33
+ require 'dailyshare/controllers/main'
34
+
35
+ module DailyShare
36
+ class App < Sinatra::Base
37
+ helpers DailyShare::Helpers
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dailyshare
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tyler Kellen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: linguistics
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: aws-s3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rmagick
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: exifr
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: mail
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: ''
95
+ email: tyler@sleekcode.net
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - .gitignore
101
+ - Gemfile
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - dailyshare.gemspec
106
+ - lib/dailyshare.rb
107
+ - lib/dailyshare/controllers/admin.rb
108
+ - lib/dailyshare/controllers/main.rb
109
+ - lib/dailyshare/helpers/entry.rb
110
+ - lib/dailyshare/helpers/helpers.rb
111
+ - lib/dailyshare/models/member.rb
112
+ - lib/dailyshare/models/photo.rb
113
+ - lib/dailyshare/version.rb
114
+ homepage: https://github.com/tkellen/ruby-dailyshare
115
+ licenses: []
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 1.8.24
135
+ signing_key:
136
+ specification_version: 3
137
+ summary: 356 Projects for Sinatra/Sequel
138
+ test_files: []