Compare commits
87 Commits
before-rai
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dabf1a520 | ||
|
|
52e7009c17 | ||
|
|
8d44a25199 | ||
|
|
e6f8d6b4cc | ||
|
|
1d9fab3942 | ||
|
|
f6d5812379 | ||
|
|
e2f33cd006 | ||
|
|
9360c63503 | ||
|
|
b381ede8e1 | ||
|
|
37fe465dd9 | ||
|
|
23317eb0fb | ||
|
|
4dc2d9d54f | ||
|
|
446a23ce3a | ||
|
|
fdd8536b5b | ||
|
|
7f0de10150 | ||
|
|
bbdabd8c53 | ||
|
|
b59415efcf | ||
|
|
949bd9c6fa | ||
|
|
d1536aa741 | ||
|
|
c5b1b87617 | ||
|
|
aa71a13c0d | ||
|
|
6f2ad538a1 | ||
|
|
c13292de2f | ||
|
|
299bd97217 | ||
|
|
359867eddd | ||
|
|
d23462fadb | ||
|
|
156f54e909 | ||
|
|
7839238026 | ||
|
|
ae106dd280 | ||
|
|
c997f03bc1 | ||
|
|
c6d1aa0efc | ||
|
|
27ca264d23 | ||
|
|
95e9b97abe | ||
|
|
cdbc636e79 | ||
|
|
f8da0db2b7 | ||
|
|
49ac794ba7 | ||
|
|
82d4378699 | ||
|
|
2c7e6adc47 | ||
|
|
061b6e2e48 | ||
|
|
547185baa2 | ||
|
|
049770056d | ||
|
|
b4a4135415 | ||
|
|
71340444c1 | ||
|
|
12281630d1 | ||
|
|
3b9ae9c297 | ||
|
|
094b648e2f | ||
|
|
346322b8d3 | ||
|
|
2a0d165d93 | ||
|
|
5cfa1175af | ||
|
|
1fd0cd2916 | ||
|
|
831b513e6f | ||
|
|
3025683638 | ||
|
|
b7ef0a5b9d | ||
|
|
9b193c217d | ||
|
|
a8c27750ef | ||
|
|
bafceda467 | ||
|
|
c5f0c79a03 | ||
|
|
65de5ee7d7 | ||
|
|
c780e6f756 | ||
|
|
e737739f51 | ||
|
|
596006e8e1 | ||
|
|
648f412ceb | ||
|
|
8674cf777f | ||
|
|
da1a13f472 | ||
|
|
ab049fb4bb | ||
|
|
6d57afb194 | ||
|
|
7d7f21b737 | ||
|
|
8367745433 | ||
|
|
e652835a5f | ||
|
|
13f60a6a1f | ||
|
|
c4a85b71d7 | ||
|
|
62082f5a82 | ||
|
|
7f729bd402 | ||
|
|
f53f2d2c63 | ||
|
|
e7a7064aca | ||
|
|
d2923b595f | ||
|
|
b7355fded8 | ||
|
|
76cc733755 | ||
|
|
ca8d8ed610 | ||
|
|
a8838761e0 | ||
|
|
e1b4787988 | ||
|
|
4ecf99a4ae | ||
|
|
02fd4a64d1 | ||
|
|
c0255bdf87 | ||
|
|
414cf485ab | ||
|
|
191158639e | ||
|
|
eddaea6395 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,5 +7,8 @@
|
|||||||
tmp
|
tmp
|
||||||
nbproject
|
nbproject
|
||||||
*.swp
|
*.swp
|
||||||
spec/test_app
|
spec/dummy
|
||||||
*.tmproj
|
*.tmproj
|
||||||
|
Gemfile.lock
|
||||||
|
Guardfile
|
||||||
|
|
||||||
|
|||||||
43
Gemfile
43
Gemfile
@@ -1,35 +1,12 @@
|
|||||||
source 'http://rubygems.org'
|
source 'http://rubygems.org'
|
||||||
|
gemspec
|
||||||
|
|
||||||
#gem 'spree_core', :path => '../spree/core'
|
group :test do
|
||||||
#gem 'spree_digital', :path => 'spree_digital'
|
if RUBY_PLATFORM.downcase.include? "darwin"
|
||||||
#gem "sqlite3-ruby"
|
gem 'guard-rspec'
|
||||||
#
|
gem 'rb-fsevent'
|
||||||
#group :test do
|
gem 'growl'
|
||||||
# gem 'rspec-rails', '= 2.5.0'
|
end
|
||||||
# gem 'factory_girl', '= 1.3.3'
|
end
|
||||||
# gem 'factory_girl_rails', '= 1.0.1'
|
|
||||||
# gem 'rcov'
|
gem 'spree', '~> 1.3.2'
|
||||||
# gem 'shoulda'
|
|
||||||
# gem 'faker'
|
|
||||||
# if RUBY_VERSION < "1.9"
|
|
||||||
# gem "ruby-debug"
|
|
||||||
# else
|
|
||||||
# gem "ruby-debug19"
|
|
||||||
# end
|
|
||||||
#end
|
|
||||||
#
|
|
||||||
#group :cucumber do
|
|
||||||
# gem 'cucumber-rails'
|
|
||||||
# gem 'database_cleaner', '= 0.6.7.RC'
|
|
||||||
# gem 'nokogiri'
|
|
||||||
# gem 'capybara', '= 0.4.1.2'
|
|
||||||
# gem 'factory_girl', '= 1.3.3'
|
|
||||||
# gem 'factory_girl_rails', '= 1.0.1'
|
|
||||||
# gem 'faker'
|
|
||||||
# gem 'launchy'
|
|
||||||
# if RUBY_VERSION < "1.9"
|
|
||||||
# gem "ruby-debug"
|
|
||||||
# else
|
|
||||||
# gem "ruby-debug19"
|
|
||||||
# end
|
|
||||||
#end
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
GEM
|
|
||||||
remote: http://rubygems.org/
|
|
||||||
specs:
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
h1. Spree Digital
|
|
||||||
|
|
||||||
This is a spree extension to enable downloadable products. The design goal is to keep it robust. It should survive future spree versions out of the box.
|
|
||||||
|
|
||||||
NOTE: This is still under development, but since things were falling into place, I thought I'd publish it.
|
|
||||||
|
|
||||||
The idea is simple. You attach a file to a Product (or a Variant of this Product) and when people buy it, they will receive a link via email where they can download it once. There are a few assumptions that spree_digital (currently) makes and it's important to be aware of them. These might change, but since I programmed digital_spree over night, this is what you get ;)
|
|
||||||
|
|
||||||
* The table structure of spree_core is not touched. Spree_digital lives parallel to spree_core and does not do any changes to your existing database, except adding two new tables.
|
|
||||||
* The download links will be sent via email in the order confirmation (or "resend" from the admin section). The links do *not* appear in the order "overview" that the customer sees.
|
|
||||||
* There should only be "live" payments. Once the order is checked-out, the download links will immediately be sent (i.e. in the order confirmation).
|
|
||||||
* You should define a shipping method called "download" and it should be cost-free. Spree_digital will use that one when all products in the cart are digital
|
|
||||||
* One may buy several items of the same digital product in one cart. The customer will simply receive several links by doing so. This allows customer's to legally purchase multiple copies of the same product and maybe give one away to a friend.
|
|
||||||
* The links will only work 3 times (but we tell the customer that it works only once). After 24 hours the links are deactivated. This should keep you reasonably free from complaints of people who are not capable of appropriately clicking on a link. They can try a whole day long, 3 times per link. The file should really be downloadable for weird customers. I understand that this is a little bit security by obscurity, but it's better than other solutions that I've seen.
|
|
||||||
* The file @views/order_mailer/confirm_email.text.erb@ is the only thing that should need customization. But since I assume you do that anyway, it doesn't hurt to do it when you're using spree_digital. The reason is that the download links are added to the confirmation email to the customer.
|
|
||||||
* A purchased product can be downloaded even if you disable the product immediately. You would have to remove the attached file in your admin section to prevent people from downloading purchased products.
|
|
||||||
* File are uploaded to @rails_root/private@. Make sure it's symlinked in case you're using Capistrano.
|
|
||||||
* We use send_file to send the files on download. Yes, it goes through the entire stack right now.
|
|
||||||
|
|
||||||
h2. Installation
|
|
||||||
|
|
||||||
I assume that you already have a Rails 3 Spree application up and running. If not, the "Spree Documentation":http://spreecommerce.com/documentation will help you getting started.
|
|
||||||
|
|
||||||
To make use of @digital_spree@, you need to add this line to your @Gemfile@:
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
gem 'spree_digital', :git => 'git://github.com/funkensturm/spree_digital.git', :branch => 'master'
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
The following terminal commands will copy the file "spree_digital/db/migration/20110410134726_create_digitals.rb":http://github.com/funkensturm/spree_digital/blob/master/db/migrate/20110410134726_create_digitals.rb to the corresponding directory in your Rails application and apply the migration to your database.
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
rake spree_digital:install
|
|
||||||
rake db:migrate
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
This should be all there is to do.
|
|
||||||
|
|
||||||
h3. Important!
|
|
||||||
|
|
||||||
You should go to the spree admin section and create a shipping method that has the word @download@ somehow in its name (it should be cost-free, but it doesn't have to). It will be detected by spree_digital. Otherwise your customer will be forced to choose something like "UPS" even if they purchase only downloadable products.
|
|
||||||
|
|
||||||
h2. Usage
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
h1. Developer Section
|
|
||||||
|
|
||||||
h3. Table Diagram
|
|
||||||
|
|
||||||
<img src="http://github.com/funkensturm/spree_digital/raw/master/doc/tables.png">
|
|
||||||
|
|
||||||
h3. Installation (for Developers)
|
|
||||||
|
|
||||||
Get the spree framework and spree_digital extension for it:
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
git clone git://github.com/spree/spree.git
|
|
||||||
git clone git://github.com/funkensturm/spree_digital
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
Go into the spree directory and run the bundle command:
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
cd spree
|
|
||||||
bundle install
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
Go into the spree_digital directory and do the same:
|
|
||||||
|
|
||||||
NOTE: At this point you may need to uncomment the stuff in the @Gemfile@ before you can start developing and testing!
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
cd spree_digital
|
|
||||||
bundle install
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
Bring up the test application (you only need to do this whenever you fiddle around with the migrations) and then you can run the tests as you please.
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
rake test_app
|
|
||||||
rake spec
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
This link may be very helpful to you: "http://github.com/spree/spree":http://github.com/spree/spree
|
|
||||||
|
|
||||||
h3. License
|
|
||||||
|
|
||||||
Copyright (c) 2011 funkensturm.
|
|
||||||
Released under the MIT License
|
|
||||||
See "http://github.com/funkensturm/spree_digital/blob/master/LICENSE":LICENSE
|
|
||||||
66
Rakefile
66
Rakefile
@@ -1,31 +1,17 @@
|
|||||||
require 'rubygems'
|
|
||||||
require 'rake'
|
require 'rake'
|
||||||
require 'rake/testtask'
|
require 'rake/testtask'
|
||||||
require 'rake/packagetask'
|
require 'rake/packagetask'
|
||||||
require 'rake/gempackagetask'
|
require 'rubygems/package_task'
|
||||||
|
require 'rspec/core/rake_task'
|
||||||
|
require 'spree/core/testing_support/common_rake'
|
||||||
|
|
||||||
gemfile = File.expand_path('../spec/test_app/Gemfile', __FILE__)
|
RSpec::Core::RakeTask.new
|
||||||
if File.exists?(gemfile) && (%w(spec cucumber).include?(ARGV.first.to_s) || ARGV.size == 0)
|
|
||||||
require 'bundler'
|
|
||||||
ENV['BUNDLE_GEMFILE'] = gemfile
|
|
||||||
Bundler.setup
|
|
||||||
|
|
||||||
require 'rspec'
|
task :default => [:spec]
|
||||||
require 'rspec/core/rake_task'
|
|
||||||
RSpec::Core::RakeTask.new
|
|
||||||
|
|
||||||
require 'cucumber/rake/task'
|
|
||||||
Cucumber::Rake::Task.new do |t|
|
|
||||||
t.cucumber_opts = %w{--format progress}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "Default Task"
|
|
||||||
task :default => [:spec, :cucumber ]
|
|
||||||
|
|
||||||
spec = eval(File.read('spree_digital.gemspec'))
|
spec = eval(File.read('spree_digital.gemspec'))
|
||||||
|
|
||||||
Rake::GemPackageTask.new(spec) do |p|
|
Gem::PackageTask.new(spec) do |p|
|
||||||
p.gem_spec = spec
|
p.gem_spec = spec
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,40 +22,18 @@ task :release => :package do
|
|||||||
Rake::Task['gem:push'].invoke
|
Rake::Task['gem:push'].invoke
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Default Task"
|
|
||||||
task :default => [ :spec ]
|
|
||||||
|
|
||||||
desc "Regenerates a rails 3 app for testing"
|
desc "Regenerates a rails 3 app for testing"
|
||||||
task :test_app do
|
task :test_app do
|
||||||
require '../spree/lib/generators/spree/test_app_generator'
|
ENV['LIB_NAME'] = 'spree_digital'
|
||||||
class SpreeDigitalTestAppGenerator < Spree::Generators::TestAppGenerator
|
|
||||||
|
require File.join `bundle show spree_core`.chomp, 'lib/generators/spree/dummy/dummy_generator.rb'
|
||||||
def install_gems
|
Spree::DummyGenerator.class_eval do
|
||||||
inside "test_app" do
|
def test_dummy_add_digital
|
||||||
run 'rake spree_core:install'
|
# pulled from: http://jumph4x.net/post/20067515804/testing-spree-1-0-x-extensions-w-other-extension
|
||||||
run 'rake spree_digital:install'
|
puts "Installing #{ENV['LIB_NAME']} migrations [required for testing]"
|
||||||
end
|
directory File.join(`bundle show #{ENV['LIB_NAME']}`.chomp, 'db', 'migrate'), File.join(dummy_path, 'db')
|
||||||
end
|
end
|
||||||
|
|
||||||
def migrate_db
|
|
||||||
run_migrations
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
def full_path_for_local_gems
|
|
||||||
<<-gems
|
|
||||||
gem 'spree_core', :path => \'#{File.join(File.dirname(__FILE__), "../spree/", "core")}\'
|
|
||||||
gem 'spree_digital', :path => \'#{File.dirname(__FILE__)}\'
|
|
||||||
gems
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
SpreeDigitalTestAppGenerator.start
|
|
||||||
end
|
|
||||||
|
|
||||||
namespace :test_app do
|
Rake::Task['common:test_app'].invoke
|
||||||
desc 'Rebuild test and cucumber databases'
|
|
||||||
task :rebuild_dbs do
|
|
||||||
system("cd spec/test_app && rake db:drop db:migrate RAILS_ENV=test && rake db:drop db:migrate RAILS_ENV=cucumber")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
12
Versionfile
12
Versionfile
@@ -1,7 +1,5 @@
|
|||||||
# This file is used to designate compatibilty with different versions of Spree
|
"0.7.x" => { :ref => "eddaea63959586d123007cbca3be5bf9c5edb1a7" }
|
||||||
# Please see http://spreecommerce.com/documentation/extensions.html#versionfile for details
|
"1.0.x" => { :ref => "a8c27750ef1cf9d0ad1a2a6ebe33307da900a5c1" }
|
||||||
|
"1.1.x" => { :branch => "1-1-stable" }
|
||||||
# Currently this works with these both:
|
"1.2.x" => { :ref => "9360c635039aaeeee18026b830aa96cc7587cd0d" }
|
||||||
|
"1.3.x" => { :branch => "master" }
|
||||||
"0.60.x" => { :branch => "master" }
|
|
||||||
"0.50.x" => { :branch => "master" }
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
class Admin::DigitalsController < Admin::BaseController
|
|
||||||
|
|
||||||
resource_controller
|
|
||||||
|
|
||||||
index.before do
|
|
||||||
@product = Product.find_by_permalink(params[:product_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
create.wants.html { redirect_to admin_product_digitals_url(@product) }
|
|
||||||
destroy.wants.html { redirect_to admin_product_digitals_url(@product) }
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
class DigitalsController < Spree::BaseController
|
|
||||||
|
|
||||||
ssl_required :show
|
|
||||||
|
|
||||||
def show
|
|
||||||
link = DigitalLink.find_by_secret(params[:secret])
|
|
||||||
if link.present? and link.digital.attachment.present?
|
|
||||||
attachment = link.digital.attachment
|
|
||||||
if link.authorize! and File.file?(attachment.path)
|
|
||||||
send_file attachment.path :filename => attachment.original_filename, :type => attachment.content_type and return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
render :unauthorized
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
10
app/controllers/spree/admin/digitals_controller.rb
Normal file
10
app/controllers/spree/admin/digitals_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module Spree
|
||||||
|
class Admin::DigitalsController < Spree::Admin::ResourceController
|
||||||
|
belongs_to "spree/product", :find_by => :permalink
|
||||||
|
|
||||||
|
protected
|
||||||
|
def location_after_save
|
||||||
|
admin_product_digitals_url(@product)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Spree::Admin::OrdersController.class_eval do
|
||||||
|
def reset_digitals
|
||||||
|
@order.reset_digital_links!
|
||||||
|
flash[:notice] = t(:downloads_reset)
|
||||||
|
redirect_to admin_order_url(@order)
|
||||||
|
end
|
||||||
|
end
|
||||||
27
app/controllers/spree/digitals_controller.rb
Normal file
27
app/controllers/spree/digitals_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
module Spree
|
||||||
|
class DigitalsController < Spree::StoreController
|
||||||
|
ssl_required :show
|
||||||
|
|
||||||
|
def show
|
||||||
|
link = DigitalLink.find_by_secret(params[:secret])
|
||||||
|
|
||||||
|
if link.present? and link.digital.attachment.present?
|
||||||
|
attachment = link.digital.attachment
|
||||||
|
|
||||||
|
# don't authorize the link unless the file exists
|
||||||
|
# the logger error will help track down customer issues easier
|
||||||
|
|
||||||
|
if File.file?(attachment.path)
|
||||||
|
if link.authorize!
|
||||||
|
send_file attachment.path, :filename => attachment.original_filename, :type => attachment.content_type and return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Rails.logger.error "Missing Digital Item: #{attachment.path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
class Digital < ActiveRecord::Base
|
|
||||||
|
|
||||||
belongs_to :variant
|
|
||||||
has_many :digital_links, :dependent => :destroy
|
|
||||||
|
|
||||||
has_attached_file :attachment, :path => ":rails_root/private/digitals/:id/:basename.:extension"
|
|
||||||
|
|
||||||
# TODO: Limit the attachment to one single file. Paperclip supports many by default :/
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
class DigitalLink < ActiveRecord::Base
|
|
||||||
|
|
||||||
belongs_to :digital
|
|
||||||
belongs_to :line_item
|
|
||||||
|
|
||||||
before_validation :set_defaults, :on => :create
|
|
||||||
|
|
||||||
# Can this link stil be used? It is valid if it's less than 24 hours old and was not accessed more than 3 times
|
|
||||||
def authorizable?
|
|
||||||
self.created_at > 1.day.ago and self.access_counter < 3
|
|
||||||
end
|
|
||||||
|
|
||||||
# This method should be called when a download is initiated.
|
|
||||||
# It returns +true+ or +false+ depending on whether the authorization is granted.
|
|
||||||
def authorize!
|
|
||||||
authorizable? && increment!(:access_counter) ? true : false
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Populating the secret automatically and zero'ing the access_counter (otherwise it might turn out to be NULL)
|
|
||||||
def set_defaults
|
|
||||||
self.secret = SecureRandom.hex(15)
|
|
||||||
self.access_counter = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
LineItem.class_eval do
|
|
||||||
|
|
||||||
has_many :digital_links
|
|
||||||
|
|
||||||
after_save :create_digital_links, :if => :digital?
|
|
||||||
|
|
||||||
# Is this item digital?
|
|
||||||
def digital?
|
|
||||||
variant.digital?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Create the download link for this item if it is digital.
|
|
||||||
def create_digital_links
|
|
||||||
digital_links.delete_all
|
|
||||||
self.quantity.times do
|
|
||||||
digital_links.create!(:digital => variant.digital)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
Order.class_eval do
|
|
||||||
|
|
||||||
# Are all products/variants of this Order to be downloaded by the customer?
|
|
||||||
def digital?
|
|
||||||
line_items.map { |item| return false unless item.digital? }
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Is at least one product/variant digital?
|
|
||||||
def some_digital?
|
|
||||||
line_items.map { |item| return true if item.digital? }
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determine which method to use for shipping of digital products.
|
|
||||||
def digital_shipping_method
|
|
||||||
rates = rate_hash
|
|
||||||
# If there is a shipping method has "Download" in its name then we take that one.
|
|
||||||
rates.each { |rate| return rate if rate[:name].downcase.include?('download') }
|
|
||||||
# Other than that, we take the first one that we find that doesn't cost anything.
|
|
||||||
rates.each { |rate| return rate if rate[:cost] == 0 }
|
|
||||||
# Well, at this point we have a problem. No shipping method is cost-free or called "download".
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
18
app/models/spree/calculator/digital_delivery.rb
Normal file
18
app/models/spree/calculator/digital_delivery.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# https://github.com/spree/spree/issues/1439
|
||||||
|
require_dependency 'spree/calculator'
|
||||||
|
|
||||||
|
module Spree
|
||||||
|
class Calculator::DigitalDelivery < Calculator::FlatRate
|
||||||
|
def self.description
|
||||||
|
I18n.t(:digital_delivery)
|
||||||
|
end
|
||||||
|
|
||||||
|
def compute(object=nil)
|
||||||
|
self.preferred_amount
|
||||||
|
end
|
||||||
|
|
||||||
|
def available?(order)
|
||||||
|
order.digital?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
app/models/spree/digital.rb
Normal file
13
app/models/spree/digital.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module Spree
|
||||||
|
class Digital < ActiveRecord::Base
|
||||||
|
belongs_to :variant
|
||||||
|
has_many :digital_links, :dependent => :destroy
|
||||||
|
|
||||||
|
has_attached_file :attachment, :path => ":rails_root/private/digitals/:id/:basename.:extension"
|
||||||
|
|
||||||
|
# TODO: Limit the attachment to one single file. Paperclip supports many by default :/
|
||||||
|
|
||||||
|
attr_accessible :variant_id, :attachment
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
34
app/models/spree/digital_link.rb
Normal file
34
app/models/spree/digital_link.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module Spree
|
||||||
|
class DigitalLink < ActiveRecord::Base
|
||||||
|
|
||||||
|
belongs_to :digital
|
||||||
|
belongs_to :line_item
|
||||||
|
|
||||||
|
validates_length_of :secret, :is => 30
|
||||||
|
validates :digital, :presence => true
|
||||||
|
|
||||||
|
before_validation :set_defaults, :on => :create
|
||||||
|
|
||||||
|
# Can this link stil be used? It is valid if it's less than 24 hours old and was not accessed more than 3 times
|
||||||
|
def authorizable?
|
||||||
|
self.created_at > Spree::DigitalConfiguration[:authorized_days].day.ago and self.access_counter < Spree::DigitalConfiguration[:authorized_clicks]
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method should be called when a download is initiated.
|
||||||
|
# It returns +true+ or +false+ depending on whether the authorization is granted.
|
||||||
|
def authorize!
|
||||||
|
authorizable? && increment!(:access_counter) ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Populating the secret automatically and zero'ing the access_counter (otherwise it might turn out to be NULL)
|
||||||
|
def set_defaults
|
||||||
|
self.secret = SecureRandom.hex(15)
|
||||||
|
self.access_counter = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_accessible :digital, :line_item, :secret
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
23
app/models/spree/line_item_decorator.rb
Normal file
23
app/models/spree/line_item_decorator.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Spree::LineItem.class_eval do
|
||||||
|
|
||||||
|
has_many :digital_links, :dependent => :destroy
|
||||||
|
after_save :create_digital_links, :if => :digital?
|
||||||
|
|
||||||
|
def digital?
|
||||||
|
variant.digital?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# TODO there is no reason to create the digital links until the order is complete
|
||||||
|
def create_digital_links
|
||||||
|
digital_links.delete_all
|
||||||
|
|
||||||
|
variant.digitals.each do |digital|
|
||||||
|
self.quantity.times do
|
||||||
|
digital_links.create!(:digital => digital)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
31
app/models/spree/order_decorator.rb
Normal file
31
app/models/spree/order_decorator.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Spree::Order.class_eval do
|
||||||
|
# all products are digital
|
||||||
|
def digital?
|
||||||
|
line_items.map { |item| return false unless item.digital? }
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def some_digital?
|
||||||
|
line_items.map { |item| return true if item.digital? }
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_digital_links!
|
||||||
|
line_items.select(&:digital?).map(&:digital_links).flatten.each do |digital_link|
|
||||||
|
digital_link.update_column :access_counter, 0
|
||||||
|
digital_link.update_column :created_at, Time.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# UPGRADE_CHECK this works as of spree 1.2.0. check function for changes on upgrade
|
||||||
|
def available_shipping_methods(display_on = nil)
|
||||||
|
return [] unless ship_address
|
||||||
|
all_methods = Spree::ShippingMethod.all_available(self, display_on)
|
||||||
|
|
||||||
|
if self.digital?
|
||||||
|
all_methods.detect { |m| m.calculator.class == Spree::Calculator::DigitalDelivery }.try { |d| [d] } || all_methods
|
||||||
|
else
|
||||||
|
all_methods
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3
app/models/spree/product_decorator.rb
Normal file
3
app/models/spree/product_decorator.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Spree::Product.class_eval do
|
||||||
|
has_many :digitals, :through => :variants_including_master
|
||||||
|
end
|
||||||
19
app/models/spree/variant_decorator.rb
Normal file
19
app/models/spree/variant_decorator.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Spree::Variant.class_eval do
|
||||||
|
has_many :digitals
|
||||||
|
after_save :destroy_digital, :if => :deleted?
|
||||||
|
|
||||||
|
# Is this variant to be downloaded by the customer?
|
||||||
|
def digital?
|
||||||
|
digitals.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# :dependent => :destroy needs to be handled manually
|
||||||
|
# spree does not delete variants, just marks them as deleted?
|
||||||
|
# optionally keep digitals around for customers who require continued access to their purchases
|
||||||
|
def destroy_digital
|
||||||
|
digitals.map &:destroy unless Spree::DigitalConfiguration[:keep_digitals]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Variant.class_eval do
|
|
||||||
|
|
||||||
has_one :digital, :dependent => :destroy
|
|
||||||
after_save :destroy_digital, :if => :deleted?
|
|
||||||
|
|
||||||
# Is this variant to be downloaded by the customer?
|
|
||||||
def digital?
|
|
||||||
digital.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Spree never deleted Digitals, that's why ":dependent => :destroy" won't work on Digital.
|
|
||||||
# We need to delete the Digital manually here as soon as the Variant is nullified.
|
|
||||||
# Otherwise you'll have orphan Digitals (and their attached files!) associated with unused Variants.
|
|
||||||
def destroy_digital
|
|
||||||
digital.destroy
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
14
app/overrides/add_digital_downloads_to_invoice.rb
Normal file
14
app/overrides/add_digital_downloads_to_invoice.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Deface::Override.new(:virtual_path => "spree/shared/_order_details",
|
||||||
|
:name => "add_digital_downloads_to_invoice",
|
||||||
|
:insert_bottom => "td[data-hook='order_item_description']",
|
||||||
|
:text => %q{
|
||||||
|
<% if order.state == 'complete' and item.variant.digital? %>
|
||||||
|
<%= content_tag(:p, :class => 'download_links') do %>
|
||||||
|
<% item.digital_links.each do |digital_link| %>
|
||||||
|
<% format = File.extname(digital_link.digital.attachment.path).downcase %>
|
||||||
|
<%= link_to t(:digital_download, :type => t("digital_format#{format}")), digital_url(:host => Spree::Config.get(:site_url), :secret => digital_link.secret), :class => "btn #{format}" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
},
|
||||||
|
:disabled => false)
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
class SpreeDigitalHooks < Spree::ThemeSupport::HookListener
|
Deface::Override.new(:virtual_path => "spree/admin/shared/_product_tabs",
|
||||||
Deface::Override.new(:virtual_path => "admin/shared/_product_tabs",
|
:name => "add_digital_versions_to_admin_product_tabs",
|
||||||
:name => "converted_admin_product_tabs_986577829",
|
|
||||||
:insert_bottom => "[data-hook='admin_product_tabs'], #admin_product_tabs[data-hook]",
|
:insert_bottom => "[data-hook='admin_product_tabs'], #admin_product_tabs[data-hook]",
|
||||||
:text => " <li<%== ' class=\"active\"' if current == \"Digital Versions\" %>>
|
:text => " <li<%== ' class=\"active\"' if current == \"Digital Versions\" %>>
|
||||||
<%= link_to t(\"digital_versions\"), admin_product_digitals_path(@product) %>
|
<%= link_to t(\"digital_versions\"), admin_product_digitals_path(@product) %>
|
||||||
</li>
|
</li>
|
||||||
",
|
",
|
||||||
:disabled => false)
|
:disabled => false)
|
||||||
end
|
|
||||||
7
app/overrides/add_reset_digitals_to_admin_orders.rb
Normal file
7
app/overrides/add_reset_digitals_to_admin_orders.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Deface::Override.new(:virtual_path => "spree/admin/shared/_order_tabs",
|
||||||
|
:name => "add_reset_digitals_to_admin_orders",
|
||||||
|
:insert_after => ".sidebar",
|
||||||
|
:text => %q{
|
||||||
|
<%= content_tag(:p, button_link_to(t(:reset_downloads), reset_digitals_admin_order_url(@order)), class: 'clear') if @order.digital? or true %>
|
||||||
|
},
|
||||||
|
:disabled => false)
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<div class="yui-g">
|
|
||||||
<div class="yui-u first">
|
|
||||||
<%= form_for(:digital, :url => { :controller => 'digitals', :action => 'create' }, :html => { :multipart => true }) do |f| %>
|
|
||||||
<fieldset>
|
|
||||||
<legend><%= Variant.model_name.human %> "<%= variant.options_text %>"</legend>
|
|
||||||
|
|
||||||
<%= f.field_container :current_file do %>
|
|
||||||
<strong><%=t 'current_file' %>:</strong><br/>
|
|
||||||
<% if variant.digital? %>
|
|
||||||
<%= render variant.digital %><%= %>
|
|
||||||
<% else %>
|
|
||||||
<%=t 'none' %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p class="form-buttons">
|
|
||||||
<% if variant.digital? %>
|
|
||||||
<%= link_to t("delete_file"), admin_product_digital_url(:id => variant.digital.id), :confirm => t('delete_file_cofirmation', :filename => variant.digital.attachment_file_name), :method => :delete %>
|
|
||||||
<% else %>
|
|
||||||
|
|
||||||
<%= f.field_container :file do %>
|
|
||||||
<%= f.label :file, t("new_file") %> <span class="required">*</span><br/>
|
|
||||||
<%= f.file_field :attachment %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= hidden_field_tag 'digital[variant_id]', variant.id %>
|
|
||||||
|
|
||||||
<%= button t("upload") %>
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div><br/>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<%#
|
|
||||||
This is basically a switch that will render the appropriate "delivery choice" partial.
|
|
||||||
When there are no physical items at all, there should be no different shipping methods to choose.
|
|
||||||
%>
|
|
||||||
<%= render @order.digital? && @order.digital_shipping_method.present? ? 'delivery_digital' : 'delivery_original' %>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<fieldset id='shipping_method'>
|
|
||||||
<legend><%= t("shipping_method") %></legend>
|
|
||||||
|
|
||||||
<%= radio_button :order, :shipping_method_id, @order.digital_shipping_method[:id] %>
|
|
||||||
<%==t 'digital_shipping', :email => current_user.email %> (<%= number_to_currency @order.digital_shipping_method[:cost] %>)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="form-buttons">
|
|
||||||
<input type="submit" class="continue button primary" value="<%=t("save_and_continue") %>"/>
|
|
||||||
</div>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<%#
|
|
||||||
NOTE: The code below is an exact duplication of `spree_core/views/checkout/_delivery.html.erb´
|
|
||||||
We overwrote the original so we retain access to it by cloning it here.
|
|
||||||
Feel free to update the code below according to the original if needed!
|
|
||||||
%>
|
|
||||||
|
|
||||||
<fieldset id='shipping_method'>
|
|
||||||
<legend><%= t("shipping_method") %></legend>
|
|
||||||
<div class="inner">
|
|
||||||
<div id="methods">
|
|
||||||
<p class="field radios">
|
|
||||||
<% @order.rate_hash.each do |shipping_method| %>
|
|
||||||
<% next if @order.digital_shipping_method && shipping_method[:id] == @order.digital_shipping_method[:id] %>
|
|
||||||
<label>
|
|
||||||
<%= radio_button(:order, :shipping_method_id, shipping_method[:id]) %>
|
|
||||||
<%= shipping_method[:name] %> <%= number_to_currency shipping_method[:cost] %>
|
|
||||||
</label><br />
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<% if Spree::Config[:shipping_instructions] && @order.rate_hash.present? %>
|
|
||||||
<p id="minstrs">
|
|
||||||
<%= form.label :special_instructions, t("shipping_instructions") %><br />
|
|
||||||
<%= form.text_area :special_instructions, :cols => 40, :rows => 7 %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="form-buttons">
|
|
||||||
<input type="submit" class="continue button primary" value="<%=t("save_and_continue") %>"/>
|
|
||||||
</div>
|
|
||||||
36
app/views/spree/admin/digitals/_form.html.erb
Normal file
36
app/views/spree/admin/digitals/_form.html.erb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<%= form_for(:digital, :url => admin_product_digitals_path(@product), :method => :create, :multipart => true ) do |f| %>
|
||||||
|
<fieldset>
|
||||||
|
<legend><%= Spree::Variant.model_name.human %> "<%= variant.options_text %>"</legend>
|
||||||
|
|
||||||
|
<%= f.field_container :current_file do %>
|
||||||
|
<strong><%=t 'files' %>:</strong>
|
||||||
|
<% if variant.digital? %>
|
||||||
|
<ul>
|
||||||
|
<% variant.digitals.each do |digital| %>
|
||||||
|
<li>
|
||||||
|
<%= render digital %>
|
||||||
|
<%= link_to t("delete_file"), admin_product_digital_url(@product, digital), :confirm => t('delete_file_cofirmation', :filename => digital.attachment_file_name), :method => :delete %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% else %>
|
||||||
|
<%=t 'none' %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p class="form-buttons">
|
||||||
|
<%= f.field_container :file do %>
|
||||||
|
<%= f.label :file, t("new_file") %> <span class="required">*</span><br/>
|
||||||
|
<%= f.file_field :attachment %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= hidden_field_tag 'digital[variant_id]', variant.id %>
|
||||||
|
|
||||||
|
<%= button t('spree_digital.upload') %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= render :partial => 'admin/shared/product_sub_menu' %>
|
<%= render :partial => 'spree/admin/shared/product_sub_menu' %>
|
||||||
<%= render :partial => 'admin/shared/product_tabs', :locals => {:current => "Digital Versions"} %>
|
<%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => "Digital Versions"} %>
|
||||||
|
|
||||||
<% if @product.has_variants? %>
|
<% if @product.has_variants? %>
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
This product has no variants.
|
This product has no variants.
|
||||||
|
|
||||||
<% if @product.master.digital? %>
|
<% if @product.master.digital? %>
|
||||||
A digital version of this product currently exists:
|
A digital version of this product currently exists:
|
||||||
<%= render @product.master.digital %>
|
<%= render @product.master.digitals %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= render 'form', :variant => @product.master %>
|
<%= render 'form', :variant => @product.master %>
|
||||||
5
app/views/spree/digitals/_digital.html.erb
Normal file
5
app/views/spree/digitals/_digital.html.erb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<% if digital.attachment_file_name.present? %>
|
||||||
|
<%= digital.attachment_file_name %> (<%= number_to_human_size(digital.attachment_file_size) %>)
|
||||||
|
<% else %>
|
||||||
|
<%=t 'broken_file' %>
|
||||||
|
<% end %>
|
||||||
9
app/views/spree/digitals/unauthorized.html.erb
Normal file
9
app/views/spree/digitals/unauthorized.html.erb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<% if defined?(SpreeFancy) %>
|
||||||
|
<% content_for :subheader do %>
|
||||||
|
<h1><%= t('.unauthorized') %></h1>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<h1><%= t('.unauthorized') %></h1>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= t('.explained') %>
|
||||||
@@ -25,8 +25,7 @@ ATTENTION! Each link will only work a SINGLE TIME!
|
|||||||
|
|
||||||
<% for item in @order.line_items %>
|
<% for item in @order.line_items %>
|
||||||
<% if item.digital? %>
|
<% if item.digital? %>
|
||||||
<%= item.variant.name %>:
|
<%= raw item.variant.name %>:
|
||||||
<%= item.digital_links.inspect %>
|
|
||||||
<% for link in item.digital_links %>
|
<% for link in item.digital_links %>
|
||||||
<%= digital_url :host => Spree::Config.get(:site_url), :secret => link.secret %>
|
<%= digital_url :host => Spree::Config.get(:site_url), :secret => link.secret %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
en:
|
en:
|
||||||
digital_versions: Digital Versions
|
|
||||||
|
|
||||||
current_file: Current File
|
|
||||||
new_file: New File
|
|
||||||
upload: Upload
|
|
||||||
delete_file: Delete this file
|
|
||||||
broken_file: Warning! this file is broken
|
broken_file: Warning! this file is broken
|
||||||
|
current_file: Current File
|
||||||
|
delete_file: Delete this file
|
||||||
delete_file_cofirmation: Are you sure you want to delete the file %{filename}?
|
delete_file_cofirmation: Are you sure you want to delete the file %{filename}?
|
||||||
|
digital_delivery: Digital Delivery
|
||||||
digital_shipping: Delivery per email to <strong>%{email}</strong>
|
digital_download: Download %{type} ↓
|
||||||
|
digital_download_links: Digital Download Links
|
||||||
|
digital_format:
|
||||||
|
mp3: Audio MP3
|
||||||
|
mobi: Kindle eBook
|
||||||
|
epub: ePub eBook
|
||||||
|
pdf: pdf eBook
|
||||||
|
digital_versions: Digital Versions
|
||||||
|
downloads_reset: Digital Downloads Reset
|
||||||
|
new_file: New File
|
||||||
|
reset_downloads: Reset Digital Downloads
|
||||||
|
spree:
|
||||||
|
digitals:
|
||||||
|
unauthorized:
|
||||||
|
explained: This download link has expired.
|
||||||
|
unauthorized: Unauthorized
|
||||||
|
spree_digital:
|
||||||
|
upload: Upload
|
||||||
|
|||||||
22
config/locales/it.yml
Normal file
22
config/locales/it.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
it:
|
||||||
|
broken_file: Attenzione! questo file sembra essere rotto
|
||||||
|
current_file: File Corrente
|
||||||
|
delete_file: Elimina questo file
|
||||||
|
delete_file_cofirmation: Sei sicuro di voler eliminare il file %{filename}?
|
||||||
|
digital_delivery: Consegna Digitale
|
||||||
|
digital_download: Download %{type} ↓
|
||||||
|
digital_download_links: Link al Download Digitale
|
||||||
|
digital_format:
|
||||||
|
mp3: Audio MP3
|
||||||
|
mobi: Kindle eBook
|
||||||
|
epub: ePub eBook
|
||||||
|
pdf: pdf eBook
|
||||||
|
digital_versions: Versioni Digitali
|
||||||
|
new_file: Nuovo File
|
||||||
|
spree:
|
||||||
|
digitals:
|
||||||
|
unauthorized:
|
||||||
|
explained: This download link has expired.
|
||||||
|
unauthorized: Unauthorized
|
||||||
|
spree_digital:
|
||||||
|
upload: Upload
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
ja:
|
ja:
|
||||||
digital_versions: デジタル版
|
|
||||||
|
|
||||||
current_file: 現在のバージョン
|
|
||||||
new_file: 新しいファイル
|
|
||||||
upload: アップロード
|
|
||||||
delete_file: このファイルを削除
|
|
||||||
broken_file: 注意! このファイルが壊れている!
|
broken_file: 注意! このファイルが壊れている!
|
||||||
|
current_file: 現在のバージョン
|
||||||
|
delete_file: このファイルを削除
|
||||||
delete_file_cofirmation: 本当に「%{filename}」を削除しても宜しいですか?
|
delete_file_cofirmation: 本当に「%{filename}」を削除しても宜しいですか?
|
||||||
|
|
||||||
digital_shipping: ダウンロードリンクを<strong>%{email}</strong>に送ります
|
digital_shipping: ダウンロードリンクを<strong>%{email}</strong>に送ります
|
||||||
|
digital_versions: デジタル版
|
||||||
|
new_file: 新しいファイル
|
||||||
|
spree:
|
||||||
|
digitals:
|
||||||
|
unauthorized:
|
||||||
|
explained: This download link has expired.
|
||||||
|
unauthorized: Unauthorized
|
||||||
|
upload: アップロード
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
Rails.application.routes.draw do
|
Spree::Core::Engine.routes.draw do
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :products do
|
resources :products do
|
||||||
resources :digitals
|
resources :digitals
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :orders do
|
||||||
|
member do
|
||||||
|
get :reset_digitals
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/digital/:secret', :to => 'digitals#show', :via => :get, :as => 'digital', :constraints => { :secret => /[a-zA-Z0-9]{30}/ }
|
get '/digital/:secret', :to => 'digitals#show', :via => :get, :as => 'digital', :constraints => { :secret => /[a-zA-Z0-9]{30}/ }
|
||||||
|
end
|
||||||
end
|
|
||||||
|
|||||||
6
db/migrate/20111207121840_rename_digital_to_namespace.rb
Normal file
6
db/migrate/20111207121840_rename_digital_to_namespace.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class RenameDigitalToNamespace < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
rename_table :digitals, :spree_digitals
|
||||||
|
rename_table :digital_links, :spree_digital_links
|
||||||
|
end
|
||||||
|
end
|
||||||
31
lib/generators/spree_digital/install/install_generator.rb
Normal file
31
lib/generators/spree_digital/install/install_generator.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module SpreeDigital
|
||||||
|
module Generators
|
||||||
|
class InstallGenerator < Rails::Generators::Base
|
||||||
|
|
||||||
|
# def add_javascripts
|
||||||
|
# append_file "app/assets/javascripts/store/all.js", "//= require store/spree_digital\n"
|
||||||
|
# append_file "app/assets/javascripts/admin/all.js", "//= require admin/spree_digital\n"
|
||||||
|
# end
|
||||||
|
|
||||||
|
# def add_stylesheets
|
||||||
|
# inject_into_file "app/assets/stylesheets/store/all.css", " *= require store/spree_digital\n",
|
||||||
|
# :before => /\*\//, :verbose => true
|
||||||
|
# inject_into_file "app/assets/stylesheets/admin/all.css", " *= require admin/spree_digital\n",
|
||||||
|
# :before => /\*\//, :verbose => true
|
||||||
|
# end
|
||||||
|
|
||||||
|
def add_migrations
|
||||||
|
run 'bundle exec rake railties:install:migrations FROM=spree_digital'
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_migrations
|
||||||
|
res = ask "Would you like to run the migrations now? [Y/n]"
|
||||||
|
if res == "" || res.downcase == "y"
|
||||||
|
run 'bundle exec rake db:migrate'
|
||||||
|
else
|
||||||
|
puts "Skipping rake db:migrate, don't forget to run it!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
lib/spree/spree_digital_configuration.rb
Normal file
12
lib/spree/spree_digital_configuration.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Spree
|
||||||
|
class SpreeDigitalConfiguration < Preferences::Configuration
|
||||||
|
# number of times a customer can download a digital file
|
||||||
|
preference :authorized_clicks, :integer, :default => 3
|
||||||
|
|
||||||
|
# number of days after initial purchase the customer can download a file
|
||||||
|
preference :authorized_days, :integer, :default => 2
|
||||||
|
|
||||||
|
# should digitals be kept around after the associated product is destroyed
|
||||||
|
preference :keep_digitals, :boolean, :default => false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,17 +1,2 @@
|
|||||||
require 'spree_core'
|
require 'spree_core'
|
||||||
require 'spree_digital_hooks'
|
require 'spree_digital/engine'
|
||||||
|
|
||||||
module SpreeDigital
|
|
||||||
class Engine < Rails::Engine
|
|
||||||
|
|
||||||
config.autoload_paths += %W(#{config.root}/lib)
|
|
||||||
|
|
||||||
def self.activate
|
|
||||||
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
|
|
||||||
Rails.env.production? ? require(c) : load(c)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
config.to_prepare &method(:activate).to_proc
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
23
lib/spree_digital/engine.rb
Normal file
23
lib/spree_digital/engine.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module SpreeDigital
|
||||||
|
class Engine < Rails::Engine
|
||||||
|
engine_name 'spree_digital'
|
||||||
|
|
||||||
|
config.autoload_paths += %W(#{config.root}/lib)
|
||||||
|
|
||||||
|
initializer "spree.spree_digital.preferences", :after => "spree.environment" do |app|
|
||||||
|
Spree::DigitalConfiguration = Spree::SpreeDigitalConfiguration.new
|
||||||
|
end
|
||||||
|
|
||||||
|
initializer "spree.register.digital_shipping", :after => 'spree.register.calculators' do |app|
|
||||||
|
app.config.spree.calculators.shipping_methods << Spree::Calculator::DigitalDelivery
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.activate
|
||||||
|
Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator*.rb")) do |c|
|
||||||
|
Rails.application.config.cache_classes ? require(c) : load(c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
config.to_prepare &method(:activate).to_proc
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace :spree_digital do
|
|
||||||
|
|
||||||
desc "Copies all migrations (NOTE: This will be obsolete with Rails 3.1)"
|
|
||||||
task :install do
|
|
||||||
Rake::Task['spree_digital:install:migrations'].invoke
|
|
||||||
end
|
|
||||||
|
|
||||||
namespace :install do
|
|
||||||
|
|
||||||
desc "Copies all migrations (NOTE: This will be obsolete with Rails 3.1)"
|
|
||||||
task :migrations do
|
|
||||||
source = File.join(File.dirname(__FILE__), '..', '..', 'db')
|
|
||||||
destination = File.join(Rails.root, 'db')
|
|
||||||
Spree::FileUtilz.mirror_files(source, destination)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# add custom rake tasks here
|
|
||||||
146
readme.md
Normal file
146
readme.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Spree Digital
|
||||||
|
|
||||||
|
This is a spree extension to enable downloadable products (ebooks, MP3s, videos, etc).
|
||||||
|
|
||||||
|
The master branch is compatible with Spree 1.2.x. 1.0.x - 1.1.x versions are available, check the `Versionfile`.
|
||||||
|
|
||||||
|
This documentation is not complete and possibly out of date in some cases. There are features that have been implemented that are not documented here, please look at the source for complete documentation.
|
||||||
|
|
||||||
|
The idea is simple. You attach a file to a Product (or a Variant of this Product) and when people buy it, they will receive a link via email where they can download it once. There are a few assumptions that spree_digital (currently) makes and it's important to be aware of them.
|
||||||
|
|
||||||
|
* The table structure of spree_core is not touched. Spree digital lives parallel to spree_core and does change the existing database, except adding two new tables.
|
||||||
|
* The download links will be sent via email in the order confirmation (or "resend" from the admin section). The links do *not* appear in the order "overview" that the customer sees. Adding download buttons to `OrdersController#show` is easy, [check out this gist](https://gist.github.com/3187793#file_add_spree_digital_buttons_to_invoice.rb).
|
||||||
|
* Once the order is checked-out, the download links will immediately be sent (i.e. in the order confirmation). You'll have to modify the system to support 'delayed' payments (like a billable account).
|
||||||
|
* You should create a ShippingMethod based on the Digital Delivery calculator type. The default cost for digital delivery is 0, but you can define a flat rate (creating a per-item digital delivery fee would be possible as well). Checkout the [source code](https://github.com/halo/spree_digital/blob/master/app/models/spree/calculator/digital_delivery.rb) for the Digital Delivery calculator for more information.
|
||||||
|
* One may buy several items of the same digital product in one cart. The customer will simply receive several links by doing so. This allows customer's to legally purchase multiple copies of the same product and maybe give one away to a friend.
|
||||||
|
* You can set how many times (clicks) the users downloads will work. You can also set how long the users links will work (expiration). For more information, [check out the preferences object](https://github.com/halo/spree_digital/blob/master/lib/spree/spree_digital_configuration.rb)
|
||||||
|
* The file `views/order_mailer/confirm_email.text.erb` is the only thing that should need customization. If you are looking for HTML emails, [this branch of spree-html-email](http://github.com/iloveitaly/spree-html-email) supports spree_digital
|
||||||
|
* A purchased product can be downloaded even if you disable the product immediately. You would have to remove the attached file in your admin section to prevent people from downloading purchased products.
|
||||||
|
* File are uploaded to `rails_root/private`. Make sure it's symlinked in case you're using Capistrano. If you want to change the upload path, [check out this gist](https://gist.github.com/3187793#file_spree_digital_path_change_decorator.rb)
|
||||||
|
* You must add a `views/spree/digitals/unauthorized.html.erb` file to customize an error message to the user if they exceed the download / days limit
|
||||||
|
* We use send_file to send the files on download. See below for instructions on how to push file downloading off to nginx.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add this line to your gemfile:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gem 'spree_digital', :git => 'git://github.com/halo/spree_digital.git', :branch => 'master'
|
||||||
|
```
|
||||||
|
|
||||||
|
The following terminal commands will copy the migration files to the corresponding directory in your Rails application and apply the migrations to your database.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bundle exec rails g spree_digital:install
|
||||||
|
bundle exec rake db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set any preferences.
|
||||||
|
|
||||||
|
### Shipping Configuration
|
||||||
|
|
||||||
|
You should create a ShippingMethod based on the Digital Delivery calculator type. It will be detected by `spree_digital`. Otherwise your customer will be forced to choose something like "UPS" even if they purchase only downloadable products.
|
||||||
|
|
||||||
|
### Improving File Downloading: `send_file` + nginx
|
||||||
|
|
||||||
|
Without customization, all file downloading will route through the rails stack. This means that if you have two workers, and two customers are downloading files, your server is maxed out and will be unresponsive until the downloads have finished.
|
||||||
|
|
||||||
|
Luckily there is an easy way around this: pass off file downloading to nginx (or apache, etc). Take a look at [this article](http://blog.kiskolabs.com/post/637725747/nginx-rails-send-file) for a good explanation.
|
||||||
|
|
||||||
|
```
|
||||||
|
# in your app's source
|
||||||
|
# config/environments/production.rb
|
||||||
|
|
||||||
|
# Specifies the header that your server uses for sending files
|
||||||
|
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
|
||||||
|
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
|
||||||
|
|
||||||
|
|
||||||
|
# on your server
|
||||||
|
# /etc/nginx/sites-available/spree-secure
|
||||||
|
upstream unicorn_spree_secure {
|
||||||
|
server unix:/data/spree/shared/sockets/unicorn.sock fail_timeout=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443;
|
||||||
|
...
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X_FORWARDED_PROTO https;
|
||||||
|
...
|
||||||
|
|
||||||
|
proxy_set_header X-Sendfile-Type X-Accel-Redirect;
|
||||||
|
proxy_set_header X-Accel-Mapping /data/spree/shared/uploaded-files/digitals/=/digitals/;
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
location /digitals/ {
|
||||||
|
internal;
|
||||||
|
root /data/spree/shared/uploaded-files/;
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
References:
|
||||||
|
|
||||||
|
* [Gist of example config](https://gist.github.com/416004)
|
||||||
|
* [Change paperclip's upload / download path](https://gist.github.com/3187793#file_spree_digital_path_change_decorator.rb)
|
||||||
|
* ["X-Accel-Mapping header missing" in nginx error log](http://stackoverflow.com/questions/6237016/message-x-accel-mapping-header-missing-in-nginx-error-log)
|
||||||
|
* [Another good, but older, explanation](http://kovyrin.net/2006/11/01/nginx-x-accel-redirect-php-rails/)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Table Diagram
|
||||||
|
|
||||||
|
<img src="http://github.com/halo/spree_digital/raw/master/doc/tables.png">
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Get the spree framework and spree_digital extension for it:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone git://github.com/spree/spree.git
|
||||||
|
git clone git://github.com/halo/spree_digital.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Go into the spree directory and run the bundle command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd spree
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
Go into the spree_digital directory and do the same:
|
||||||
|
|
||||||
|
NOTE: At this point you may need to uncomment the stuff in the `Gemfile` before you can start developing and testing!
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd spree_digital
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
Bring up the test application (you only need to do this whenever you fiddle around with the migrations) and then you can run the tests as you please.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rake test_app
|
||||||
|
rake spec
|
||||||
|
```
|
||||||
|
|
||||||
|
This link may be very helpful to you: [http://github.com/spree/spree](http://github.com/spree/spree)
|
||||||
|
|
||||||
|
### Authors
|
||||||
|
|
||||||
|
* [iloveitaly](http://github.com/iloveitaly/)
|
||||||
|
* [halo](http://github.com/halo)
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
Copyright (c) 2011 funkensturm.
|
||||||
|
Released under the MIT License
|
||||||
|
See [LICENSE](http://github.com/funkensturm/spree_digital/blob/master/LICENSE)
|
||||||
43
spec/controllers/digitals_controller_spec.rb
Normal file
43
spec/controllers/digitals_controller_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Spree::Admin::DigitalsController do
|
||||||
|
before do
|
||||||
|
@product = FactoryGirl.create :product
|
||||||
|
|
||||||
|
controller.stub :authorize! => true
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:product) { @product }
|
||||||
|
|
||||||
|
context "with variants" do
|
||||||
|
before do
|
||||||
|
# TODO add variants with digital
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should display an empty page when no digitals exist" do
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should display list of digitals when they exist" do
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without variants" do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
it "should display an empty page when no digitals exist" do
|
||||||
|
spree_get :index, "product_id" => product.permalink
|
||||||
|
response.code.should == "200"
|
||||||
|
response.body.should include("This product has no variants")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should display list of digitals when they exist" do
|
||||||
|
@digital = FactoryGirl.create :digital, :variant => product.master
|
||||||
|
spree_get :index, :product_id => product.permalink
|
||||||
|
response.code.should == "200"
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
require 'factory_girl'
|
|
||||||
|
|
||||||
Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
|
|
||||||
fp = File.expand_path(f)
|
|
||||||
require fp
|
|
||||||
end
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
Factory.define :digital do |f|
|
FactoryGirl.define do
|
||||||
f.variant { |p| p.association(:variant) }
|
factory :digital, :class => Spree::Digital do |f|
|
||||||
|
# TODO good to assign variant association if no association is manually defined
|
||||||
|
# f.variant { |p| p.association(:variant) }
|
||||||
|
|
||||||
|
attachment_content_type 'application/octet-stream'
|
||||||
|
attachment_file_name 'a_great_book.epub'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
Factory.define :digital_link do |f|
|
FactoryGirl.define do
|
||||||
f.digital { |p| p.association(:digital) }
|
factory :digital_link, :class => Spree::DigitalLink do |f|
|
||||||
f.line_item { |p| p.association(:line_item) }
|
f.digital { |p| p.association(:digital) }
|
||||||
|
f.line_item { |p| p.association(:line_item) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
12
spec/factories/digital_shipping_factory.rb
Normal file
12
spec/factories/digital_shipping_factory.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FactoryGirl.define do
|
||||||
|
# https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#modifying-factories
|
||||||
|
|
||||||
|
factory :digital_shipping_calculator, class: Spree::Calculator::DigitalDelivery do |c|
|
||||||
|
after_create { |c| c.set_preference(:amount, 0) }
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :digital_shipping_method, parent: :shipping_method do |f|
|
||||||
|
name "Digital Delivery"
|
||||||
|
calculator { FactoryGirl.build :digital_shipping_calculator }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,98 +1,75 @@
|
|||||||
require File.dirname(__FILE__) + '/../spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe DigitalLink do
|
describe Spree::DigitalLink do
|
||||||
|
|
||||||
context 'validation' do
|
context 'validation' do
|
||||||
it { should belong_to(:digital) }
|
it { should belong_to(:digital) }
|
||||||
it { should belong_to(:line_item) }
|
it { should belong_to(:line_item) }
|
||||||
it { should have_valid_factory(:digital_link) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#create" do
|
context "#create" do
|
||||||
it "should have an appropriately long secret" do
|
it "should have an appropriately long secret" do
|
||||||
Factory(:digital_link).secret.length.should == 30
|
FactoryGirl.create(:digital_link).secret.length.should == 30
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should have the access counter being an Integer on zero" do
|
it "should have the access counter being an Integer on zero" do
|
||||||
Factory(:digital_link).access_counter.should == 0
|
FactoryGirl.create(:digital_link).access_counter.should == 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#update" do
|
context "#update" do
|
||||||
it "should not change the secret when updated" do
|
it "should not change the secret when updated" do
|
||||||
digital_link = Factory(:digital_link)
|
digital_link = FactoryGirl.create(:digital_link)
|
||||||
secret = digital_link.secret
|
secret = digital_link.secret
|
||||||
digital_link.increment(:access_counter).save
|
digital_link.increment(:access_counter).save
|
||||||
digital_link.secret.should == secret
|
digital_link.secret.should == secret
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should enforce to have an associated digital" do
|
it "should enforce to have an associated digital" do
|
||||||
link = Factory(:digital_link)
|
link = FactoryGirl.create(:digital_link)
|
||||||
lambda { link.update_attributes!(:digital => nil) }.should raise_error(ActiveRecord::RecordInvalid)
|
lambda { link.update_attributes!(:digital => nil) }.should raise_error(ActiveRecord::RecordInvalid)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not allow an empty or too short secret" do
|
it "should not allow an empty or too short secret" do
|
||||||
link = Factory(:digital_link)
|
link = FactoryGirl.create(:digital_link)
|
||||||
lambda { link.update_attributes!(:secret => nil) }.should raise_error(ActiveRecord::RecordInvalid)
|
lambda { link.update_attributes!(:secret => nil) }.should raise_error(ActiveRecord::RecordInvalid)
|
||||||
lambda { link.update_attributes!(:secret => 'x' * 25) }.should raise_error(ActiveRecord::RecordInvalid)
|
lambda { link.update_attributes!(:secret => 'x' * 25) }.should raise_error(ActiveRecord::RecordInvalid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#context "authorization" do
|
context "authorization" do
|
||||||
# it "should increment the counter using #authorize!" do
|
it "should increment the counter using #authorize!" do
|
||||||
# link = Factory(:digital_link)
|
link = FactoryGirl.create(:digital_link)
|
||||||
# link.authorize!
|
link.access_counter.should == 0
|
||||||
# link.access_counter.should == 1
|
link.authorize!
|
||||||
# end
|
link.access_counter.should == 1
|
||||||
#
|
end
|
||||||
# it "should not be #authorized? when the access_counter is too high" do
|
|
||||||
# link = Factory(:digital_link)
|
it "should not be #authorized? when the access_counter is too high" do
|
||||||
# link.stub(:access_counter => 2)
|
link = FactoryGirl.create(:digital_link)
|
||||||
# link.authorized?.should be_true
|
link.stub(:access_counter => Spree::DigitalConfiguration[:authorized_clicks] - 1)
|
||||||
# link.stub(:access_counter => 3)
|
link.authorizable?.should be_true
|
||||||
# link.authorized?.should be_false
|
link.stub(:access_counter => Spree::DigitalConfiguration[:authorized_clicks])
|
||||||
# end
|
link.authorizable?.should be_false
|
||||||
#
|
end
|
||||||
# it "should not be #authorize! when the access_counter is too high" do
|
|
||||||
# link = Factory(:digital_link)
|
it "should not be #authorize! when the created_at date is too far in the past" do
|
||||||
# link.stub(:access_counter => 2)
|
link = FactoryGirl.create(:digital_link)
|
||||||
# link.authorize!.should be_true
|
link.authorize!.should be_true
|
||||||
# link.stub(:access_counter => 3)
|
link.stub(:created_at => (Spree::DigitalConfiguration[:authorized_days] * 24 - 1).hours.ago)
|
||||||
# link.authorize!.should be_false
|
link.authorize!.should be_true
|
||||||
# end
|
link.stub(:created_at => (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago)
|
||||||
#
|
link.authorize!.should be_false
|
||||||
# it "should not be #authorized? when the created_at date is too far in the past" do
|
end
|
||||||
# link = Factory(:digital_link)
|
|
||||||
# link.authorized?.should be_true
|
it "should not be #authorized? when both access_counter and created_at are invalid" do
|
||||||
# link.stub(:created_at => 23.hours.ago)
|
link = FactoryGirl.create(:digital_link)
|
||||||
# link.authorized?.should be_true
|
link.authorizable?.should be_true
|
||||||
# link.stub(:created_at => 25.hours.ago)
|
link.stub(:access_counter => Spree::DigitalConfiguration[:authorized_clicks], :created_at => (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago)
|
||||||
# link.authorized?.should be_false
|
link.authorizable?.should be_false
|
||||||
# end
|
end
|
||||||
#
|
|
||||||
# it "should not be #authorize! when the created_at date is too far in the past" do
|
end
|
||||||
# link = Factory(:digital_link)
|
|
||||||
# link.authorize!.should be_true
|
|
||||||
# link.stub(:created_at => 23.hours.ago)
|
|
||||||
# link.authorize!.should be_true
|
|
||||||
# link.stub(:created_at => 25.hours.ago)
|
|
||||||
# link.authorize!.should be_false
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "should not be #authorized? when both access_counter and created_at are invalid" do
|
|
||||||
# link = Factory(:digital_link)
|
|
||||||
# link.authorized?.should be_true
|
|
||||||
# link.stub(:access_counter => 3, :created_at => 25.hours.ago)
|
|
||||||
# link.authorized?.should be_false
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "should not be #authorize! when both access_counter and created_at are invalid" do
|
|
||||||
# link = Factory(:digital_link)
|
|
||||||
# link.authorize!.should be_true
|
|
||||||
# link.stub(:access_counter => 3, :created_at => 25.hours.ago)
|
|
||||||
# link.authorize!.should be_false
|
|
||||||
# end
|
|
||||||
#end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
require File.dirname(__FILE__) + '/../spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Digital do
|
describe Spree::Digital do
|
||||||
|
|
||||||
context 'validation' do
|
context 'validation' do
|
||||||
it { should belong_to(:variant) }
|
it { should belong_to(:variant) }
|
||||||
it { should have_valid_factory(:digital) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#create" do
|
context "#create" do
|
||||||
@@ -12,14 +11,13 @@ describe Digital do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "#destroy" do
|
context "#destroy" do
|
||||||
#it "should destroy associated digital_links" do
|
it "should destroy associated digital_links" do
|
||||||
# digital = Factory(:digital)
|
digital = FactoryGirl.create(:digital)
|
||||||
# 3.times { digital.digital_links.create! :order => Factory(:order) }
|
3.times { digital.digital_links.create!({ :line_item => FactoryGirl.create(:line_item) }, :without_protection => true) }
|
||||||
# DigitalLink.count.should == 3
|
Spree::DigitalLink.count.should == 3
|
||||||
# digital.destroy
|
digital.destroy
|
||||||
# DigitalLink.count.should == 0
|
Spree::DigitalLink.count.should == 0
|
||||||
#end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
require File.dirname(__FILE__) + '/../spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe LineItem do
|
describe Spree::LineItem do
|
||||||
|
|
||||||
context "#save" do
|
context "#save" do
|
||||||
it "should create one link for a single digital Variant" do
|
it "should create one link for a single digital Variant" do
|
||||||
digital_variant = Factory(:variant, :digital => Factory(:digital))
|
digital_variant = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
line_item = Factory(:line_item, :variant => digital_variant)
|
line_item = FactoryGirl.create(:line_item, :variant => digital_variant)
|
||||||
links = digital_variant.digital.digital_links
|
links = digital_variant.digitals.first.digital_links
|
||||||
links.all.size.should == 1
|
links.all.size.should == 1
|
||||||
links.first.line_item.should == line_item
|
links.first.line_item.should == line_item
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should create a link for each quantity of a digital Variant, even when quantity changes later" do
|
it "should create a link for each quantity of a digital Variant, even when quantity changes later" do
|
||||||
digital_variant = Factory(:variant, :digital => Factory(:digital))
|
digital_variant = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
line_item = Factory(:line_item, :variant => digital_variant, :quantity => 5)
|
line_item = FactoryGirl.create(:line_item, :variant => digital_variant, :quantity => 5)
|
||||||
links = digital_variant.digital.digital_links
|
links = digital_variant.digitals.first.digital_links
|
||||||
links.all.size.should == 5
|
links.all.size.should == 5
|
||||||
links.each { |link| link.line_item.should == line_item }
|
links.each { |link| link.line_item.should == line_item }
|
||||||
|
|
||||||
# quantity update
|
# quantity update
|
||||||
line_item.quantity = 8
|
line_item.quantity = 8
|
||||||
line_item.save
|
line_item.save
|
||||||
links = digital_variant.digital.digital_links
|
links = digital_variant.digitals.first.digital_links
|
||||||
links.all.size.should == 8
|
links.all.size.should == 8
|
||||||
links.each { |link| link.line_item.should == line_item }
|
links.each { |link| link.line_item.should == line_item }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should create a link for digital variants with multiple digital downloads attached" do
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
require File.dirname(__FILE__) + '/../spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Order do
|
describe Spree::Order do
|
||||||
|
|
||||||
context 'validation' do
|
|
||||||
it { should have_valid_factory(:order) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "#add_variant" do
|
context "#add_variant" do
|
||||||
it "should add digital Variants of quantity 1 to an order" do
|
it "should add digital Variants of quantity 1 to an order" do
|
||||||
order = Factory(:order)
|
order = FactoryGirl.create(:order)
|
||||||
order.add_variant variant1 = Factory(:variant, :digital => Factory(:digital))
|
order.add_variant variant1 = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
order.add_variant variant2 = Factory(:variant, :digital => Factory(:digital))
|
order.add_variant variant2 = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
order.add_variant variant3 = Factory(:variant, :digital => Factory(:digital))
|
order.add_variant variant3 = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
order.line_items.first.variant.should == variant1
|
order.line_items.first.variant.should == variant1
|
||||||
order.line_items.second.variant.should == variant2
|
order.line_items.second.variant.should == variant2
|
||||||
order.line_items.third.variant.should == variant3
|
order.line_items.third.variant.should == variant3
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should handle quantity higher than 1 when adding one specific digital Variant" do
|
it "should handle quantity higher than 1 when adding one specific digital Variant" do
|
||||||
order = Factory(:order)
|
order = FactoryGirl.create(:order)
|
||||||
digital_variant = Factory(:variant, :digital => Factory(:digital))
|
digital_variant = FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
order.add_variant digital_variant, 3
|
order.add_variant digital_variant, 3
|
||||||
order.line_items.first.quantity.should == 3
|
order.line_items.first.quantity.should == 3
|
||||||
order.add_variant digital_variant, 2
|
order.add_variant digital_variant, 2
|
||||||
@@ -29,26 +24,61 @@ describe Order do
|
|||||||
|
|
||||||
context "line_item analysis" do
|
context "line_item analysis" do
|
||||||
it "should understand that all products are digital" do
|
it "should understand that all products are digital" do
|
||||||
order = Factory(:order)
|
order = FactoryGirl.create(:order)
|
||||||
3.times do
|
3.times do
|
||||||
order.add_variant Factory(:variant, :digital => Factory(:digital))
|
order.add_variant FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
end
|
end
|
||||||
order.digital?.should be_true
|
order.digital?.should be_true
|
||||||
order.add_variant Factory(:variant, :digital => Factory(:digital)), 4
|
order.add_variant FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)]), 4
|
||||||
order.digital?.should be_true
|
order.digital?.should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should understand that not all products are digital" do
|
it "should understand that not all products are digital" do
|
||||||
order = Factory(:order)
|
order = FactoryGirl.create(:order)
|
||||||
3.times do
|
3.times do
|
||||||
order.add_variant Factory(:variant, :digital => Factory(:digital))
|
order.add_variant FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)])
|
||||||
end
|
end
|
||||||
order.add_variant Factory(:variant) # this is the analog product
|
order.add_variant FactoryGirl.create(:variant) # this is the analog product
|
||||||
order.digital?.should be_false
|
order.digital?.should be_false
|
||||||
order.add_variant Factory(:variant, :digital => Factory(:digital)), 4
|
order.add_variant FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)]), 4
|
||||||
order.digital?.should be_false
|
order.digital?.should be_false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "digital shipping" do
|
||||||
|
before do
|
||||||
|
@order = FactoryGirl.create(:order)
|
||||||
|
# need shipp_address for rate_hash.count != 0
|
||||||
|
@order.ship_address = FactoryGirl.create :address
|
||||||
|
@order.bill_address = FactoryGirl.create :address
|
||||||
|
@order.save!
|
||||||
|
|
||||||
|
3.times { @order.add_variant FactoryGirl.create(:variant, :digitals => [FactoryGirl.create(:digital)]) }
|
||||||
|
|
||||||
|
FactoryGirl.create :digital_shipping_method
|
||||||
|
s = FactoryGirl.create :shipping_method
|
||||||
|
s.calculator.set_preference(:amount, 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:order) { @order }
|
||||||
|
|
||||||
|
it "should only offer digital shipping if all items are digital" do
|
||||||
|
order.digital?.should be_true
|
||||||
|
order.rate_hash.count.should == 1
|
||||||
|
order.rate_hash.first.shipping_method.calculator.class.should == Spree::Calculator::DigitalDelivery
|
||||||
|
order.rate_hash.first.cost.should == 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not offer digital shipping if only some items are digital" do
|
||||||
|
order.digital?.should be_true
|
||||||
|
order.add_variant FactoryGirl.create(:variant) # this is the analog product
|
||||||
|
order.digital?.should be_false
|
||||||
|
|
||||||
|
order.rate_hash.count.should == 1
|
||||||
|
order.rate_hash.first.shipping_method.calculator.class.should_not == Spree::Calculator::DigitalDelivery
|
||||||
|
order.rate_hash.first.cost.should == 10.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
38
spec/models/variant_spec.rb
Normal file
38
spec/models/variant_spec.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Spree::Variant do
|
||||||
|
context "#destroy" do
|
||||||
|
before do
|
||||||
|
@variant = FactoryGirl.create :variant
|
||||||
|
@digital = FactoryGirl.create :digital, :variant => @variant
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:variant) { @variant }
|
||||||
|
let(:digital) { @digital }
|
||||||
|
|
||||||
|
it "should destroy associated digitals by default" do
|
||||||
|
# default is false
|
||||||
|
Spree::DigitalConfiguration[:keep_digitals] = false
|
||||||
|
|
||||||
|
Spree::Digital.count.should == 1
|
||||||
|
variant.digitals.present?.should be_true
|
||||||
|
variant.deleted_at = Time.now
|
||||||
|
variant.deleted?.should be_true
|
||||||
|
variant.save!
|
||||||
|
expect { digital.reload.present? }.to raise_error
|
||||||
|
Spree::Digital.count.should == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should conditionally keep associated digitals" do
|
||||||
|
Spree::DigitalConfiguration[:keep_digitals] = true
|
||||||
|
|
||||||
|
Spree::Digital.count.should == 1
|
||||||
|
variant.digitals.present?.should be_true
|
||||||
|
variant.deleted_at = Time.now
|
||||||
|
variant.save!
|
||||||
|
variant.deleted?.should be_true
|
||||||
|
expect { digital.reload.present? }.to_not raise_error
|
||||||
|
Spree::Digital.count.should == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,61 +1,40 @@
|
|||||||
require File.expand_path("../factories", __FILE__)
|
ENV["RAILS_ENV"] = "test"
|
||||||
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
# NOTE: The rest of file is the same as spree/core/spec/spec_helper.rb #
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
|
|
||||||
# from the project root directory.
|
|
||||||
ENV["RAILS_ENV"] ||= 'test'
|
|
||||||
require File.expand_path("../test_app/config/environment", __FILE__)
|
|
||||||
require 'rspec/rails'
|
require 'rspec/rails'
|
||||||
|
require 'database_cleaner'
|
||||||
|
require 'ffaker'
|
||||||
|
require 'shoulda-matchers'
|
||||||
|
|
||||||
# Requires supporting files with custom matchers and macros, etc,
|
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f }
|
||||||
# in ./support/ and its subdirectories.
|
|
||||||
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
|
||||||
|
|
||||||
require 'spree_core/testing_support/factories'
|
require 'spree/core/testing_support/factories'
|
||||||
|
require 'spree/core/testing_support/controller_requests'
|
||||||
|
require 'spree/core/url_helpers'
|
||||||
|
|
||||||
|
Dir[File.join(File.dirname(__FILE__), "factories/*.rb")].each {|f| require f }
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
# == Mock Framework
|
|
||||||
#
|
|
||||||
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
|
||||||
#
|
|
||||||
# config.mock_with :mocha
|
|
||||||
# config.mock_with :flexmock
|
|
||||||
# config.mock_with :rr
|
|
||||||
config.mock_with :rspec
|
config.mock_with :rspec
|
||||||
|
config.include FactoryGirl::Syntax::Methods
|
||||||
|
config.include Spree::Core::UrlHelpers
|
||||||
|
config.include Spree::Core::TestingSupport::ControllerRequests
|
||||||
|
config.use_transactional_fixtures = false
|
||||||
|
|
||||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
config.before(:each) do
|
||||||
|
if example.metadata[:js]
|
||||||
#config.include Devise::TestHelpers, :type => :controller
|
DatabaseCleaner.strategy = :truncation, { :except => ['spree_countries', 'spree_zones', 'spree_zone_members', 'spree_states', 'spree_roles'] }
|
||||||
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
else
|
||||||
# examples within a transaction, comment the following line or assign false
|
DatabaseCleaner.strategy = :transaction
|
||||||
# instead of true.
|
end
|
||||||
config.use_transactional_fixtures = true
|
|
||||||
end
|
|
||||||
|
|
||||||
@configuration ||= AppConfiguration.find_or_create_by_name("Default configuration")
|
|
||||||
|
|
||||||
PAYMENT_STATES = Payment.state_machine.states.keys unless defined? PAYMENT_STATES
|
|
||||||
SHIPMENT_STATES = Shipment.state_machine.states.keys unless defined? SHIPMENT_STATES
|
|
||||||
ORDER_STATES = Order.state_machine.states.keys unless defined? ORDER_STATES
|
|
||||||
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# context "factory" do
|
|
||||||
# it { should have_valid_factory(:address) }
|
|
||||||
# end
|
|
||||||
RSpec::Matchers.define :have_valid_factory do |factory_name|
|
|
||||||
match do |model|
|
|
||||||
Factory(factory_name).new_record?.should be_false
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
config.before(:each) do
|
||||||
|
DatabaseCleaner.start
|
||||||
|
# reset_spree_preferences
|
||||||
|
end
|
||||||
|
|
||||||
|
config.after(:each) do
|
||||||
|
DatabaseCleaner.clean
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,15 +1,26 @@
|
|||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.platform = Gem::Platform::RUBY
|
s.platform = Gem::Platform::RUBY
|
||||||
s.name = 'spree_digital'
|
s.name = 'spree_digital'
|
||||||
s.version = '0.60.99'
|
s.version = '1.1.1'
|
||||||
s.summary = ''
|
s.summary = ''
|
||||||
s.description = 'This gem is supposed to be used in connection with spree_core. It was tested with spree_core 0.66.99 but it might work with newer versions as well.'
|
s.description = 'Digital download functionality for spree'
|
||||||
s.author = 'funkensturm.'
|
s.authors = ['funkensturm', 'Michael Bianco']
|
||||||
|
s.email = ['info@cliffsidedev.com']
|
||||||
s.homepage = 'http://www.funkensturm.com'
|
s.homepage = 'http://www.funkensturm.com'
|
||||||
s.files = `git ls-files`.split("\n")
|
s.files = `git ls-files`.split("\n")
|
||||||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||||
s.require_path = 'lib'
|
s.require_path = 'lib'
|
||||||
s.requirements << 'none'
|
s.requirements << 'none'
|
||||||
s.required_ruby_version = '>= 1.8.7'
|
s.required_ruby_version = '>= 1.8.7'
|
||||||
s.add_dependency('spree_core')
|
|
||||||
|
s.add_dependency 'spree_core', '~> 1.3.0'
|
||||||
|
|
||||||
|
# test suite
|
||||||
|
s.add_development_dependency 'shoulda-matchers'
|
||||||
|
s.add_development_dependency 'capybara'
|
||||||
|
s.add_development_dependency 'factory_girl_rails', '~> 1.7.0'
|
||||||
|
s.add_development_dependency 'rspec-rails', '~> 2.8'
|
||||||
|
s.add_development_dependency 'sqlite3'
|
||||||
|
s.add_development_dependency 'ffaker'
|
||||||
|
s.add_development_dependency 'database_cleaner'
|
||||||
end
|
end
|
||||||
Reference in New Issue
Block a user