dailyshare 0.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/.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: []