This commit is contained in:
Captain Future
2011-04-17 16:26:36 +02:00
commit e6134722d4
39 changed files with 1199 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
\#*
*~
.#*
.DS_Store
.idea
.project
tmp
nbproject
*.swp
spec/test_app
.tmproj

35
Gemfile Normal file
View File

@@ -0,0 +1,35 @@
source 'http://rubygems.org'
#gem 'spree_core', :path => '../spree/core'
#gem 'spree_digital', :path => 'spree_digital'
#gem "sqlite3-ruby"
#
#group :test do
# gem 'rspec-rails', '= 2.5.0'
# gem 'factory_girl', '= 1.3.3'
# gem 'factory_girl_rails', '= 1.0.1'
# gem 'rcov'
# 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

217
Gemfile.lock Normal file
View File

@@ -0,0 +1,217 @@
PATH
remote: /Users/Future/Sites/Rails/Projekte/spree/core
specs:
spree_core (0.60.99)
activemerchant (= 1.12.0)
acts_as_list (= 0.1.2)
faker (= 0.9.5)
highline (= 1.5.1)
jquery-rails (= 0.2.6)
meta_search (= 1.0.1)
nested_set (= 1.6.4)
paperclip (= 2.3.8)
rails (= 3.0.6)
rd_find_by_param (= 0.1.1)
rd_resource_controller
rd_unobtrusive_date_picker (= 0.1.0)
state_machine (= 0.9.4)
stringex (= 1.0.3)
will_paginate (= 3.0.pre2)
PATH
remote: .
specs:
spree_digital (0.60.99)
spree_core (>= 0.60.99)
GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
actionmailer (3.0.6)
actionpack (= 3.0.6)
mail (~> 2.2.15)
actionpack (3.0.6)
activemodel (= 3.0.6)
activesupport (= 3.0.6)
builder (~> 2.1.2)
erubis (~> 2.6.6)
i18n (~> 0.5.0)
rack (~> 1.2.1)
rack-mount (~> 0.6.14)
rack-test (~> 0.5.7)
tzinfo (~> 0.3.23)
activemerchant (1.12.0)
activesupport (>= 2.3.8)
braintree (>= 2.0.0)
builder (>= 2.0.0)
activemodel (3.0.6)
activesupport (= 3.0.6)
builder (~> 2.1.2)
i18n (~> 0.5.0)
activerecord (3.0.6)
activemodel (= 3.0.6)
activesupport (= 3.0.6)
arel (~> 2.0.2)
tzinfo (~> 0.3.23)
activeresource (3.0.6)
activemodel (= 3.0.6)
activesupport (= 3.0.6)
activesupport (3.0.6)
acts_as_list (0.1.2)
arel (2.0.9)
braintree (2.9.1)
builder
builder (2.1.2)
capybara (0.4.1.2)
celerity (>= 0.7.9)
culerity (>= 0.2.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
selenium-webdriver (>= 0.0.27)
xpath (~> 0.1.3)
celerity (0.8.9)
childprocess (0.1.8)
ffi (~> 1.0.6)
columnize (0.3.2)
configuration (1.2.0)
cucumber (0.10.2)
builder (>= 2.1.2)
diff-lcs (>= 1.1.2)
gherkin (>= 2.3.5)
json (>= 1.4.6)
term-ansicolor (>= 1.0.5)
cucumber-rails (0.4.1)
cucumber (>= 0.10.1)
nokogiri (>= 1.4.4)
rack-test (>= 0.5.7)
culerity (0.2.15)
database_cleaner (0.6.7.RC)
diff-lcs (1.1.2)
erubis (2.6.6)
abstract (>= 1.0.0)
factory_girl (1.3.3)
factory_girl_rails (1.0.1)
factory_girl (~> 1.3)
railties (>= 3.0.0)
faker (0.9.5)
i18n (~> 0.4)
ffi (1.0.7)
rake (>= 0.8.7)
gherkin (2.3.5)
json (>= 1.4.6)
highline (1.5.1)
i18n (0.5.0)
jquery-rails (0.2.6)
rails (~> 3.0)
thor (~> 0.14.4)
json (1.5.1)
json_pure (1.5.1)
launchy (0.4.0)
configuration (>= 0.0.5)
rake (>= 0.8.1)
linecache (0.43)
mail (2.2.15)
activesupport (>= 2.3.6)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
meta_search (1.0.1)
actionpack (~> 3.0.2)
activerecord (~> 3.0.2)
activesupport (~> 3.0.2)
arel (~> 2.0.2)
mime-types (1.16)
nested_set (1.6.4)
activerecord (>= 3.0.0)
railties (>= 3.0.0)
nokogiri (1.4.4)
paperclip (2.3.8)
activerecord
activesupport
polyglot (0.3.1)
rack (1.2.2)
rack-mount (0.6.14)
rack (>= 1.0.0)
rack-test (0.5.7)
rack (>= 1.0)
rails (3.0.6)
actionmailer (= 3.0.6)
actionpack (= 3.0.6)
activerecord (= 3.0.6)
activeresource (= 3.0.6)
activesupport (= 3.0.6)
bundler (~> 1.0)
railties (= 3.0.6)
railties (3.0.6)
actionpack (= 3.0.6)
activesupport (= 3.0.6)
rake (>= 0.8.7)
thor (~> 0.14.4)
rake (0.8.7)
rcov (0.9.9)
rd_find_by_param (0.1.1)
activerecord (~> 3.0)
activesupport (~> 3.0)
rd_resource_controller (1.0.1)
rd_unobtrusive_date_picker (0.1.0)
rspec (2.5.0)
rspec-core (~> 2.5.0)
rspec-expectations (~> 2.5.0)
rspec-mocks (~> 2.5.0)
rspec-core (2.5.1)
rspec-expectations (2.5.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.5.0)
rspec-rails (2.5.0)
actionpack (~> 3.0)
activesupport (~> 3.0)
railties (~> 3.0)
rspec (~> 2.5.0)
ruby-debug (0.10.4)
columnize (>= 0.1)
ruby-debug-base (~> 0.10.4.0)
ruby-debug-base (0.10.4)
linecache (>= 0.3)
rubyzip (0.9.4)
selenium-webdriver (0.1.4)
childprocess (>= 0.1.7)
ffi (>= 1.0.7)
json_pure
rubyzip
shoulda (2.11.3)
sqlite3 (1.3.3)
sqlite3-ruby (1.3.3)
sqlite3 (>= 1.3.3)
state_machine (0.9.4)
stringex (1.0.3)
term-ansicolor (1.0.5)
thor (0.14.6)
treetop (1.4.9)
polyglot (>= 0.3.1)
tzinfo (0.3.26)
will_paginate (3.0.pre2)
xpath (0.1.3)
nokogiri (~> 1.3)
PLATFORMS
ruby
DEPENDENCIES
capybara (= 0.4.1.2)
cucumber-rails
database_cleaner (= 0.6.7.RC)
factory_girl (= 1.3.3)
factory_girl_rails (= 1.0.1)
faker
launchy
nokogiri
rcov
rspec-rails (= 2.5.0)
ruby-debug
shoulda
spree_core!
spree_digital!
sqlite3-ruby

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2011 funkensturm.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

114
README.textile Normal file
View File

@@ -0,0 +1,114 @@
h2. 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.
h3. Introduction
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.
h3. Installation
Add something like this to your @Gemfile@ in your brand new Rails 3 application:
<pre>
gem 'spree'
gem 'spree_social', :git => 'git://github.com/funkensturm/spree_digital.git', :branch => 'master'
</pre>
If you prefer to use specific versions, feel free to do so. This configuration, for instance, determines some versions manually:
<pre>
gem 'rails', '= 3.0.5'
gem 'spree', '= 0.50.0'
gem 'spree_social', :git => 'git://github.com/funkensturm/spree_digital.git', :branch => 'master'
</pre>
The following terminal commands are corresponding to the spree README at "github.com/spree/spree":http://github.com/spree/spree and are needed to bring up spree:
<pre>
bundle install
rails g spree:site
rake spree:install
rake db:migrate
</pre>
Of course you may already have a spree application running. But *either way* you will have to run these two commands at this point:
<pre>
rake spree_digital:install # This one will copy the migration file from spree_digital to your rails_root/db/migrate
rake db:migrate
</pre>
This should be it. You might want to populate your store with sample data like so:
<pre>
rake spree_sample:install
rake db:seed
rake db:sample
</pre>
h4. Important Notice
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.
h3. 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 "LICENSE":LICENSE

75
Rakefile Normal file
View File

@@ -0,0 +1,75 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/packagetask'
require 'rake/gempackagetask'
gemfile = File.expand_path('../spec/test_app/Gemfile', __FILE__)
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'
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'))
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
end
desc "Release to gemcutter"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
Rake::Task['gem:push'].invoke
end
desc "Default Task"
task :default => [ :spec ]
desc "Regenerates a rails 3 app for testing"
task :test_app do
require '../spree/lib/generators/spree/test_app_generator'
class SpreeDigitalTestAppGenerator < Spree::Generators::TestAppGenerator
def install_gems
inside "test_app" do
run 'rake spree_core:install'
run 'rake spree_digital:install'
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
SpreeDigitalTestAppGenerator.start
end
namespace :test_app do
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

7
Versionfile Normal file
View File

@@ -0,0 +1,7 @@
# This file is used to designate compatibilty with different versions of Spree
# Please see http://spreecommerce.com/documentation/extensions.html#versionfile for details
# Currently this works with these both:
"0.60.x" => { :branch => "master" }
"0.50.x" => { :branch => "master" }

View File

@@ -0,0 +1,12 @@
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

View File

@@ -0,0 +1,16 @@
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/models/digital.rb Normal file
View File

@@ -0,0 +1,10 @@
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

View File

@@ -0,0 +1,27 @@
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

View File

@@ -0,0 +1,22 @@
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

View File

@@ -0,0 +1,20 @@
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
# 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

View File

@@ -0,0 +1,20 @@
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

View 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 %>

View File

@@ -0,0 +1,35 @@
<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.human_name %> "<%= 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/>

View File

@@ -0,0 +1,20 @@
<%= render :partial => 'admin/shared/product_sub_menu' %>
<%= render :partial => 'admin/shared/product_tabs', :locals => {:current => "Digital Versions"} %>
<% if @product.has_variants? %>
<% for variant in @product.variants do %>
<%= render 'form', :variant => variant %>
<% end %>
<% else %>
This product has no variants.
<% if @product.master.digital? %>
A digital version of this product currently exists:
<%= render @product.master.digital %>
<% end %>
<%= render 'form', :variant => @product.master %>
<% end %>

View File

@@ -0,0 +1,5 @@
<%#
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' %>

View File

@@ -0,0 +1,12 @@
<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>

View File

@@ -0,0 +1,32 @@
<%#
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 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>

View File

@@ -0,0 +1,36 @@
Dear Customer,
Please review and retain the following order information for your records.
============================================================
Order Summary
============================================================
<% for item in @order.line_items %>
<%= item.variant.sku %> <%=item.variant.product.name%> <%= variant_options(item.variant) %> (<%=item.quantity%>) @ <%= number_to_currency item.price %> = <%= number_to_currency(item.price * item.quantity) %>
<% end %>
============================================================
Subtotal: <%= number_to_currency @order.item_total %>
<% @order.adjustments.each do |adjustment| %>
<%= "#{adjustment.label}: #{number_to_currency adjustment.amount}"%>
<% end %>
Order Total: <%= number_to_currency @order.total %>
============================================================
Download links for digital products
============================================================
ATTENTION! Each link will only work a SINGLE TIME!
Also, they will only work WITHIN 24 HOURS!
<% for item in @order.line_items %>
<% if item.digital? %>
<%= item.variant.name %>:
<% for link in item.digital_links %>
<%= digital_url :protocol => 'https', :host => "yourdomainxygoeshere.com", :secret => link.secret %>
<% end %>
<% end %>
<% end %>
============================================================
Thank you for your business.

12
config/locales/en.yml Normal file
View File

@@ -0,0 +1,12 @@
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
delete_file_cofirmation: Are you sure you want to delete the file %{filename}?
digital_shipping: Delivery per email to <strong>%{email}</strong>

11
config/routes.rb Normal file
View File

@@ -0,0 +1,11 @@
Rails.application.routes.draw do
namespace :admin do
resources :products do
resources :digitals
end
end
get '/digital/:secret', :to => 'digitals#show', :via => :get, :as => 'digital', :constraints => { :secret => /[a-zA-Z0-9]{30}/ }
end

View File

@@ -0,0 +1,30 @@
class CreateDigitals < ActiveRecord::Migration
def self.up
create_table :digitals do |t|
t.integer :variant_id
t.string :attachment_file_name
t.string :attachment_content_type
t.integer :attachment_file_size
t.timestamps
end
add_index :digitals, :variant_id
create_table :digital_links, :force => true do |t|
t.integer :digital_id
t.integer :line_item_id
t.string :secret
t.integer :access_counter
t.timestamps
end
add_index :digital_links, :digital_id
add_index :digital_links, :line_item_id
add_index :digital_links, :secret
end
def self.down
drop_table :digitals
drop_table :digital_links
end
end

BIN
doc/tables.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

17
lib/spree_digital.rb Normal file
View File

@@ -0,0 +1,17 @@
require 'spree_core'
require 'spree_digital_hooks'
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

View File

@@ -0,0 +1,11 @@
class SpreeDigitalHooks < Spree::ThemeSupport::HookListener
insert_after :admin_product_tabs do
<<-END
<li<%== ' class="active"' if current == "Digital Versions" %>>
<%= link_to t("digital_versions"), admin_product_digitals_path(@product) %>
</li>
END
end
end

18
lib/tasks/install.rake Normal file
View File

@@ -0,0 +1,18 @@
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

View File

@@ -0,0 +1 @@
# add custom rake tasks here

6
spec/factories.rb Normal file
View File

@@ -0,0 +1,6 @@
require 'factory_girl'
Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
fp = File.expand_path(f)
require fp
end

View File

@@ -0,0 +1,3 @@
Factory.define :digital do |f|
f.variant { |p| p.association(:variant) }
end

View File

@@ -0,0 +1,4 @@
Factory.define :digital_link do |f|
f.digital { |p| p.association(:digital) }
f.line_item { |p| p.association(:line_item) }
end

View File

@@ -0,0 +1,98 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe DigitalLink do
context 'validation' do
it { should belong_to(:digital) }
it { should belong_to(:line_item) }
it { should have_valid_factory(:digital_link) }
end
context "#create" do
it "should have an appropriately long secret" do
Factory(:digital_link).secret.length.should == 30
end
it "should have the access counter being an Integer on zero" do
Factory(:digital_link).access_counter.should == 0
end
end
context "#update" do
it "should not change the secret when updated" do
digital_link = Factory(:digital_link)
secret = digital_link.secret
digital_link.increment(:access_counter).save
digital_link.secret.should == secret
end
it "should enforce to have an associated digital" do
link = Factory(:digital_link)
lambda { link.update_attributes!(:digital => nil) }.should raise_error(ActiveRecord::RecordInvalid)
end
it "should not allow an empty or too short secret" do
link = Factory(:digital_link)
lambda { link.update_attributes!(:secret => nil) }.should raise_error(ActiveRecord::RecordInvalid)
lambda { link.update_attributes!(:secret => 'x' * 25) }.should raise_error(ActiveRecord::RecordInvalid)
end
end
#context "authorization" do
# it "should increment the counter using #authorize!" do
# link = Factory(:digital_link)
# link.authorize!
# link.access_counter.should == 1
# end
#
# it "should not be #authorized? when the access_counter is too high" do
# link = Factory(:digital_link)
# link.stub(:access_counter => 2)
# link.authorized?.should be_true
# link.stub(:access_counter => 3)
# link.authorized?.should be_false
# end
#
# it "should not be #authorize! when the access_counter is too high" do
# link = Factory(:digital_link)
# link.stub(:access_counter => 2)
# link.authorize!.should be_true
# link.stub(:access_counter => 3)
# link.authorize!.should be_false
# end
#
# it "should not be #authorized? when the created_at date is too far in the past" do
# link = Factory(:digital_link)
# link.authorized?.should be_true
# link.stub(:created_at => 23.hours.ago)
# link.authorized?.should be_true
# link.stub(:created_at => 25.hours.ago)
# link.authorized?.should be_false
# end
#
# it "should not be #authorize! when the created_at date is too far in the past" do
# 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

View File

@@ -0,0 +1,25 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe Digital do
context 'validation' do
it { should belong_to(:variant) }
it { should have_valid_factory(:digital) }
end
context "#create" do
end
context "#destroy" do
#it "should destroy associated digital_links" do
# digital = Factory(:digital)
# 3.times { digital.digital_links.create! :order => Factory(:order) }
# DigitalLink.count.should == 3
# digital.destroy
# DigitalLink.count.should == 0
#end
end
end

View File

@@ -0,0 +1,33 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe LineItem do
context "#save" do
it "should create one link for a single digital Variant" do
digital_variant = Factory(:variant, :digital => Factory(:digital))
line_item = Factory(:line_item, :variant => digital_variant)
links = digital_variant.digital.digital_links
links.all.size.should == 1
links.first.line_item.should == line_item
end
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))
line_item = Factory(:line_item, :variant => digital_variant, :quantity => 5)
links = digital_variant.digital.digital_links
links.all.size.should == 5
links.each { |link| link.line_item.should == line_item }
# quantity update
line_item.quantity = 8
line_item.save
links = digital_variant.digital.digital_links
links.all.size.should == 8
links.each { |link| link.line_item.should == line_item }
end
end
end

54
spec/models/order_spec.rb Normal file
View File

@@ -0,0 +1,54 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe Order do
context 'validation' do
it { should have_valid_factory(:order) }
end
context "#add_variant" do
it "should add digital Variants of quantity 1 to an order" do
order = Factory(:order)
order.add_variant variant1 = Factory(:variant, :digital => Factory(:digital))
order.add_variant variant2 = Factory(:variant, :digital => Factory(:digital))
order.add_variant variant3 = Factory(:variant, :digital => Factory(:digital))
order.line_items.first.variant.should == variant1
order.line_items.second.variant.should == variant2
order.line_items.third.variant.should == variant3
end
it "should handle quantity higher than 1 when adding one specific digital Variant" do
order = Factory(:order)
digital_variant = Factory(:variant, :digital => Factory(:digital))
order.add_variant digital_variant, 3
order.line_items.first.quantity.should == 3
order.add_variant digital_variant, 2
order.line_items.first.quantity.should == 5
end
end
context "line_item analysis" do
it "should understand that all products are digital" do
order = Factory(:order)
3.times do
order.add_variant Factory(:variant, :digital => Factory(:digital))
end
order.digital?.should be_true
order.add_variant Factory(:variant, :digital => Factory(:digital)), 4
order.digital?.should be_true
end
it "should understand that not all products are digital" do
order = Factory(:order)
3.times do
order.add_variant Factory(:variant, :digital => Factory(:digital))
end
order.add_variant Factory(:variant) # this is the analog product
order.digital?.should be_false
order.add_variant Factory(:variant, :digital => Factory(:digital)), 4
order.digital?.should be_false
end
end
end

61
spec/spec_helper.rb Normal file
View File

@@ -0,0 +1,61 @@
require File.expand_path("../factories", __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'
# Requires supporting files with custom matchers and macros, etc,
# in ./support/ and its subdirectories.
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
require 'spree_core/testing_support/factories'
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.fixture_path = "#{::Rails.root}/spec/fixtures"
#config.include Devise::TestHelpers, :type => :controller
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, comment the following line or assign false
# instead of true.
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

15
spree_digital.gemspec Normal file
View File

@@ -0,0 +1,15 @@
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'spree_digital'
s.version = '0.60.99'
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.author = 'funkensturm.'
s.homepage = 'http://www.funkensturm.com'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.require_path = 'lib'
s.requirements << 'none'
s.required_ruby_version = '>= 1.8.7'
s.add_dependency('spree_core')
end

48
spree_digital.tmproj Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>currentDocument</key>
<string>Gemfile</string>
<key>documents</key>
<array>
<dict>
<key>expanded</key>
<true/>
<key>name</key>
<string>spree_digital</string>
<key>regexFolderFilter</key>
<string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
<key>sourceDirectory</key>
<string></string>
</dict>
</array>
<key>fileHierarchyDrawerWidth</key>
<integer>262</integer>
<key>metaData</key>
<dict>
<key>Gemfile</key>
<dict>
<key>caret</key>
<dict>
<key>column</key>
<integer>19</integer>
<key>line</key>
<integer>4</integer>
</dict>
<key>firstVisibleColumn</key>
<integer>0</integer>
<key>firstVisibleLine</key>
<integer>0</integer>
</dict>
</dict>
<key>openDocuments</key>
<array>
<string>Gemfile</string>
</array>
<key>showFileHierarchyDrawer</key>
<true/>
<key>windowFrame</key>
<string>{{8, 81}, {992, 680}}</string>
</dict>
</plist>