├── .github
└── stale.yml
├── .rspec
├── .github_changelog_generator
├── doc
└── tables.png
├── spec
├── support
│ ├── active_model_mocks.rb
│ ├── shoulda.rb
│ └── testing_support.rb
├── fixtures
│ └── thinking-cat.jpg
├── models
│ ├── spree
│ │ ├── product_spec.rb
│ │ ├── digital_spec.rb
│ │ ├── drm_record_spec.rb
│ │ ├── stock
│ │ │ └── splitter
│ │ │ │ └── digital_splitter_spec.rb
│ │ ├── variant_spec.rb
│ │ ├── line_item_spec.rb
│ │ ├── digital_link_spec.rb
│ │ └── order_spec.rb
│ └── calculator
│ │ └── digital_delivery_spec.rb
├── controllers
│ ├── admin
│ │ ├── orders_controller_spec.rb
│ │ └── digitals_controller_spec.rb
│ └── digitals_controller_spec.rb
└── spec_helper.rb
├── lib
├── solidus_digital
│ ├── version.rb
│ ├── testing_support
│ │ └── factories.rb
│ └── engine.rb
├── solidus_digital.rb
├── generators
│ └── solidus_digital
│ │ └── install
│ │ └── install_generator.rb
└── spree
│ └── solidus_digital_configuration.rb
├── .rubocop.yml
├── CHANGELOG.md
├── .gem_release.yml
├── bin
├── rake
├── setup
├── rails
├── rails-sandbox
├── console
├── rails-engine
└── sandbox
├── Rakefile
├── app
├── assets
│ ├── javascripts
│ │ └── spree
│ │ │ ├── backend
│ │ │ └── solidus_digital.js
│ │ │ └── frontend
│ │ │ └── solidus_digital.js
│ └── stylesheets
│ │ └── spree
│ │ ├── backend
│ │ └── solidus_digital.css
│ │ └── frontend
│ │ └── solidus_digital.css
├── views
│ └── spree
│ │ ├── digitals
│ │ ├── _digital.html.erb
│ │ └── unauthorized.html.erb
│ │ ├── admin
│ │ └── digitals
│ │ │ ├── _digital.html.erb
│ │ │ ├── index.html.erb
│ │ │ └── _form.html.erb
│ │ ├── shared
│ │ └── _digital_download_links.html.erb
│ │ └── order_mailer
│ │ └── confirm_email.text.erb
├── models
│ ├── solidus_digital
│ │ └── permission_sets
│ │ │ ├── digital_management.rb
│ │ │ └── digital_display.rb
│ └── spree
│ │ ├── digital.rb
│ │ ├── calculator
│ │ └── shipping
│ │ │ └── digital_delivery.rb
│ │ ├── drm_record.rb
│ │ ├── stock
│ │ └── splitter
│ │ │ └── digital_splitter.rb
│ │ └── digital_link.rb
├── overrides
│ └── spree
│ │ ├── shared
│ │ └── add_digital_downloads_to_invoice.rb
│ │ └── admin
│ │ └── shared
│ │ ├── _order_submenu
│ │ └── add_digital_versions_to_admin_product_tabs.html.erb.deface
│ │ └── _product_tabs
│ │ └── add_digital_versions_to_admin_product_tabs.html.erb.deface
├── decorators
│ ├── models
│ │ └── solidus_digital
│ │ │ └── spree
│ │ │ ├── product_decorator.rb
│ │ │ ├── line_item_decorator.rb
│ │ │ ├── variant_decorator.rb
│ │ │ └── order_decorator.rb
│ └── controllers
│ │ └── solidus_digital
│ │ └── spree
│ │ └── admin
│ │ └── orders_controller_decorator.rb
├── controllers
│ └── spree
│ │ ├── admin
│ │ └── digitals_controller.rb
│ │ └── digitals_controller.rb
└── services
│ └── spree
│ └── digital_links_creator.rb
├── db
└── migrate
│ ├── 20170308121840_add_drm_flag_to_digitals.rb
│ ├── 20111207121840_rename_digital_to_namespace.rb
│ ├── 20170308134726_create_drm_records.rb
│ └── 20110410134726_create_digitals.rb
├── .gitignore
├── config
├── routes.rb
└── locales
│ ├── ja.yml
│ ├── it.yml
│ ├── de.yml
│ ├── es.yml
│ └── en.yml
├── Versionfile
├── LICENSE.md
├── solidus_digital.gemspec
├── LICENSE
├── Gemfile
├── .circleci
└── config.yml
├── .rubocop_todo.yml
└── README.md
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | _extends: .github
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.github_changelog_generator:
--------------------------------------------------------------------------------
1 | issues=false
2 | exclude-labels=infrastructure
3 |
--------------------------------------------------------------------------------
/doc/tables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidusio-contrib/solidus_digital/HEAD/doc/tables.png
--------------------------------------------------------------------------------
/spec/support/active_model_mocks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rspec/active_model/mocks'
4 |
--------------------------------------------------------------------------------
/lib/solidus_digital/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | VERSION = '1.7.6'
5 | end
6 |
--------------------------------------------------------------------------------
/spec/fixtures/thinking-cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidusio-contrib/solidus_digital/HEAD/spec/fixtures/thinking-cat.jpg
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from: .rubocop_todo.yml
2 |
3 | require:
4 | - solidus_dev_support/rubocop
5 |
6 | AllCops:
7 | NewCops: disable
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | See https://github.com/solidusio-contrib/solidus_digital/releases or OLD_CHANGELOG.md for older versions.
4 |
--------------------------------------------------------------------------------
/.gem_release.yml:
--------------------------------------------------------------------------------
1 | bump:
2 | recurse: false
3 | file: 'lib/solidus_digital/version.rb'
4 | message: Bump SolidusDigital to %{version}
5 | tag: true
6 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "rubygems"
5 | require "bundler/setup"
6 |
7 | load Gem.bin_path("rake", "rake")
8 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | gem install bundler --conservative
7 | bundle update
8 | bin/rake clobber
9 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | if %w[g generate].include? ARGV.first
4 | exec "#{__dir__}/rails-engine", *ARGV
5 | else
6 | exec "#{__dir__}/rails-sandbox", *ARGV
7 | end
8 |
--------------------------------------------------------------------------------
/lib/solidus_digital.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spree/solidus_digital_configuration'
4 | require 'solidus_digital/version'
5 | require 'solidus_digital/engine'
6 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require 'solidus_dev_support/rake_tasks'
5 | SolidusDevSupport::RakeTasks.install
6 |
7 | task default: 'extension:specs'
8 |
--------------------------------------------------------------------------------
/app/assets/javascripts/spree/backend/solidus_digital.js:
--------------------------------------------------------------------------------
1 | // Placeholder manifest file.
2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js'
--------------------------------------------------------------------------------
/app/assets/javascripts/spree/frontend/solidus_digital.js:
--------------------------------------------------------------------------------
1 | // Placeholder manifest file.
2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js'
--------------------------------------------------------------------------------
/app/assets/stylesheets/spree/backend/solidus_digital.css:
--------------------------------------------------------------------------------
1 | /*
2 | Placeholder manifest file.
3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4 | */
5 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spree/frontend/solidus_digital.css:
--------------------------------------------------------------------------------
1 | /*
2 | Placeholder manifest file.
3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4 | */
5 |
--------------------------------------------------------------------------------
/spec/support/shoulda.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'shoulda-matchers'
4 |
5 | Shoulda::Matchers.configure do |config|
6 | config.integrate do |with|
7 | with.test_framework :rspec
8 | with.library :rails
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20170308121840_add_drm_flag_to_digitals.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddDrmFlagToDigitals < ActiveRecord::Migration[4.2]
4 | def change
5 | add_column :spree_digitals, :drm, :boolean, default: false, null: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | \#*
3 | *~
4 | .#*
5 | .DS_Store
6 | .idea
7 | .project
8 | .sass-cache
9 | coverage
10 | Gemfile.lock
11 | Gemfile-local
12 | tmp
13 | nbproject
14 | pkg
15 | *.swp
16 | spec/dummy
17 | spec/examples.txt
18 | /sandbox
19 | .rvmrc
20 | .ruby-version
21 | .ruby-gemset
22 |
--------------------------------------------------------------------------------
/app/views/spree/digitals/_digital.html.erb:
--------------------------------------------------------------------------------
1 | <% if digital.attachment_file_name.present? %>
2 | <%= "DRM" if digital.drm %> <%= digital.attachment_file_name %> (<%= number_to_human_size(digital.attachment_file_size) %>)
3 | <% else %>
4 | <%= I18n.t('spree.digitals.broken_file') %>
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/db/migrate/20111207121840_rename_digital_to_namespace.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RenameDigitalToNamespace < ActiveRecord::Migration[4.2]
4 | def change
5 | rename_table :digitals, :spree_digitals
6 | rename_table :digital_links, :spree_digital_links
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/spree/admin/digitals/_digital.html.erb:
--------------------------------------------------------------------------------
1 | <% if digital.attachment_file_name.present? %>
2 | <%= "DRM" if digital.drm %> <%= digital.attachment_file_name %> (<%= number_to_human_size(digital.attachment_file_size) %>)
3 | <% else %>
4 | <%= I18n.t('spree.digitals.broken_file') %>
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/models/solidus_digital/permission_sets/digital_management.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module PermissionSets
5 | class DigitalManagement < ::Spree::PermissionSets::Base
6 | def activate!
7 | can :manage, ::Spree::Digital
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/models/solidus_digital/permission_sets/digital_display.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module PermissionSets
5 | class DigitalDisplay < ::Spree::PermissionSets::Base
6 | def activate!
7 | can [:display, :admin], ::Spree::Digital
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/overrides/spree/shared/add_digital_downloads_to_invoice.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Deface::Override.new(
4 | virtual_path: "spree/shared/_order_details",
5 | name: "add_digital_downloads_to_invoice",
6 | insert_bottom: "td[data-hook='order_item_description']",
7 | partial: "spree/shared/digital_download_links"
8 | )
9 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/shared/_order_submenu/add_digital_versions_to_admin_product_tabs.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 | <% if can?(:update, @order) && @order.digital? %>
3 |
4 | <%= link_to I18n.t('spree.digitals.reset_downloads'), reset_digitals_admin_order_url(@order) %>
5 |
6 | <% end %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/views/spree/digitals/unauthorized.html.erb:
--------------------------------------------------------------------------------
1 | <% if defined?(SpreeFancy) %>
2 | <% content_for :subheader do %>
3 | <%= I18n.t('spree.digitals.unauthorized.unauthorized') %>
4 | <% end %>
5 | <% else %>
6 | <%= I18n.t('spree.digitals.unauthorized.unauthorized') %>
7 | <% end %>
8 |
9 | <%= I18n.t('spree.digitals.unauthorized.explained') %>
10 |
--------------------------------------------------------------------------------
/bin/rails-sandbox:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | app_root = 'sandbox'
4 |
5 | unless File.exist? "#{app_root}/bin/rails"
6 | warn 'Creating the sandbox app...'
7 | Dir.chdir "#{__dir__}/.." do
8 | system "#{__dir__}/sandbox" or begin
9 | warn 'Automatic creation of the sandbox app failed'
10 | exit 1
11 | end
12 | end
13 | end
14 |
15 | Dir.chdir app_root
16 | exec 'bin/rails', *ARGV
17 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/shared/_product_tabs/add_digital_versions_to_admin_product_tabs.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= content_tag :li, class: ('active' if current == 'Digital Versions') do %>
4 | <%= link_to t('spree.digitals.digital_versions'), admin_product_digitals_path(@product) %>
5 | <% end if can?(:admin, Spree::Digital) %>
6 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_digital/spree/product_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Spree
5 | module ProductDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | has_many :digitals, through: :variants_including_master
9 | end
10 | end
11 |
12 | ::Spree::Product.prepend self
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spree::Core::Engine.routes.draw do
4 | namespace :admin do
5 | resources :products do
6 | resources :digitals
7 | end
8 |
9 | resources :orders do
10 | member do
11 | get :reset_digitals
12 | end
13 | end
14 | end
15 |
16 | get '/digital/:secret', to: 'digitals#show', as: 'digital', constraints: { secret: /[a-zA-Z0-9]{30}/ }
17 | end
18 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # frozen_string_literal: true
4 |
5 | require "bundler/setup"
6 | require "solidus_digital"
7 |
8 | # You can add fixtures and/or initialization code here to make experimenting
9 | # with your gem easier. You can also use a different console, if you like.
10 | $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11 |
12 | # (If you use this, don't forget to add pry to your Gemfile!)
13 | # require "pry"
14 | # Pry.start
15 |
16 | require "irb"
17 | IRB.start(__FILE__)
18 |
--------------------------------------------------------------------------------
/spec/support/testing_support.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module TestingSupport
5 | module Helpers
6 | def image(filename)
7 | File.open(SolidusDigital::Engine.root + "spec/fixtures" + filename)
8 | end
9 |
10 | def upload_image(filename)
11 | fixture_file_upload(image(filename).path)
12 | end
13 | end
14 | end
15 | end
16 |
17 | RSpec.configure do |config|
18 | config.include SolidusDigital::TestingSupport::Helpers
19 | end
20 |
--------------------------------------------------------------------------------
/config/locales/ja.yml:
--------------------------------------------------------------------------------
1 | ja:
2 | broken_file: 注意! このファイルが壊れている!
3 | current_file: 現在のバージョン
4 | delete_file: このファイルを削除
5 | delete_file_confirmation: 本当に「%{filename}」を削除しても宜しいですか?
6 | digital_shipping: ダウンロードリンクを%{email}に送ります
7 | digital_versions: デジタル版
8 | new_file: 新しいファイル
9 | file_is_not_ready: File is processing, please stand by
10 | spree:
11 | digitals:
12 | unauthorized:
13 | explained: This download link has expired.
14 | unauthorized: Unauthorized
15 | upload: アップロード
16 |
--------------------------------------------------------------------------------
/bin/rails-engine:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path('..', __dir__)
6 | ENGINE_PATH = File.expand_path('../lib/solidus_digital/engine', __dir__)
7 |
8 | # Set up gems listed in the Gemfile.
9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11 |
12 | require 'rails/all'
13 | require 'rails/engine/commands'
14 |
--------------------------------------------------------------------------------
/db/migrate/20170308134726_create_drm_records.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateDrmRecords < ActiveRecord::Migration[4.2]
4 | def change
5 | create_table :spree_drm_records do |t|
6 | t.integer :digital_id
7 | t.integer :line_item_id
8 | t.string :attachment_file_name
9 | t.string :attachment_content_type
10 | t.integer :attachment_file_size
11 | t.timestamps
12 | end
13 | add_index :spree_drm_records, :digital_id
14 | add_index :spree_drm_records, :line_item_id
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/decorators/controllers/solidus_digital/spree/admin/orders_controller_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Spree
5 | module Admin
6 | module OrdersControllerDecorator
7 | def reset_digitals
8 | load_order
9 | @order.reset_digital_links!
10 | flash[:notice] = I18n.t('spree.digitals.downloads_reset')
11 | redirect_to spree.edit_admin_order_path(@order)
12 | end
13 |
14 | ::Spree::Admin::OrdersController.prepend self
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/models/spree/product_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Product do
6 | let(:product) { create(:product) }
7 | let(:digitals) { 3.times.map { create(:digital) } }
8 | let!(:variants) do
9 | digitals.map { |d| create(:variant, product: product, digitals: [d]) }
10 | end
11 |
12 | context 'digitals' do
13 | it 'returns the digitals from the variants' do
14 | product_digitals = product.digitals
15 | digitals.each { |d| expect(product_digitals).to include(d) }
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/Versionfile:
--------------------------------------------------------------------------------
1 | "0.7.x" => { :ref => "eddaea63959586d123007cbca3be5bf9c5edb1a7" }
2 | "1.0.x" => { :ref => "a8c27750ef1cf9d0ad1a2a6ebe33307da900a5c1" }
3 | "1.1.x" => { :branch => "1-1-stable" }
4 | "1.2.x" => { :ref => "9360c635039aaeeee18026b830aa96cc7587cd0d" }
5 | "1.3.x" => { :branch => "1-3-stable" }
6 | "2.0.x" => { :branch => "2-0-stable" }
7 | "2.1.x" => { :branch => "2-1-stable" }
8 | "2.2.x" => { :branch => "2-2-stable" }
9 | "2.3.x" => { :branch => "2-3-stable" }
10 | "2.4.x" => { :branch => "2-4-stable" }
11 | "3.0.x" => { :branch => "3-0-stable" }
12 | "3.1.x" => { :branch => "master" }
13 |
--------------------------------------------------------------------------------
/spec/models/spree/digital_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Digital do
6 | context 'validation' do
7 | it { is_expected.to belong_to(:variant) }
8 | end
9 |
10 | describe "#destroy" do
11 | it "destroys associated digital_links" do
12 | digital = create(:digital)
13 | 3.times { digital.digital_links.create!(line_item: create(:line_item)) }
14 | expect(Spree::DigitalLink.count).to eq(3)
15 | expect {
16 | digital.destroy
17 | }.to change(Spree::DigitalLink, :count).by(-3)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_digital/spree/line_item_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Spree
5 | module LineItemDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | has_many :digital_links, dependent: :destroy
9 | end
10 | end
11 |
12 | def digital?
13 | variant.digital? || variant.product.master.digital?
14 | end
15 |
16 | def create_digital_links
17 | ::Spree::DigitalLinksCreator.new(self).create_digital_links
18 | end
19 |
20 | ::Spree::LineItem.prepend self
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/views/spree/admin/digitals/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => "Digital Versions"} %>
2 |
3 | <% if @product.has_variants? %>
4 | <% for variant in @product.variants_including_master do %>
5 | <%= render 'form', :variant => variant %>
6 | <% end %>
7 | <% else %>
8 | <%= t('spree.digitals.product_no_variants') %>
9 | <% if @product.master.digital? %>
10 | <% if can?(:display, Spree::Digital) %>
11 | <%= t('spree.digitals.product_with_variants') %>
12 | <%= render @product.master.digitals %>
13 | <% end %>
14 | <% end %>
15 | <%= render 'form', :variant => @product.master %>
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/app/models/spree/digital.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | class Digital < ApplicationRecord
5 | belongs_to :variant
6 | has_many :digital_links, dependent: :destroy
7 | has_many :drm_records, dependent: :destroy
8 |
9 | has_attached_file :attachment, path: ":rails_root/private/digitals/:id/:basename.:extension", s3_permissions: :private, s3_headers: { content_disposition: 'attachment' }
10 | do_not_validate_attachment_file_type :attachment
11 | validates_attachment_presence :attachment
12 |
13 | def cloud?
14 | attachment.options[:storage] == :s3
15 | end
16 |
17 | def create_drm_record(line_item)
18 | drm_records.create!(line_item: line_item)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/spree/shared/_digital_download_links.html.erb:
--------------------------------------------------------------------------------
1 | <% if @order.complete? && item.variant.digital? %>
2 |
3 |
4 | <% item.digital_links.each do |digital_link| %>
5 | <% next if digital_link.attachment.nil? %>
6 | <% if digital_link.ready? %>
7 | <% format = File.extname(digital_link.attachment.path).downcase %>
8 | -
9 | <%= link_to I18n.t('spree.digitals.digital_download', filename: raw(digital_link.attachment_file_name)), digital_url(secret: digital_link.secret), class: "#{format}" %>
10 |
11 | <% else %>
12 | <%= I18n.t('spree.digitals.file_is_not_ready') %>
13 | <% end %>
14 | <% end %>
15 |
16 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/app/models/spree/calculator/shipping/digital_delivery.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # https://github.com/spree/spree/issues/1439
4 | require_dependency 'spree/shipping_calculator'
5 |
6 | module Spree
7 | module Calculator::Shipping
8 | class DigitalDelivery < ShippingCalculator
9 | preference :amount, :decimal, default: 0
10 | preference :currency, :string, default: ->{ Spree::Config[:currency] }
11 |
12 | def self.description
13 | I18n.t('spree.digitals.digital_delivery')
14 | end
15 |
16 | def compute_package(_package = nil)
17 | preferred_amount
18 | end
19 |
20 | def available?(package)
21 | package.contents.all? { |content| content.variant.digital? }
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/spree/drm_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | class DrmRecord < ApplicationRecord
5 | belongs_to :digital
6 | belongs_to :line_item
7 |
8 | after_create :prepare_drm_mark
9 |
10 | has_attached_file :attachment, path: ":rails_root/private/digitals/drm/:id/:basename.:extension"
11 | do_not_validate_attachment_file_type :attachment
12 |
13 | if Paperclip::Attachment.default_options[:storage] == :s3
14 | attachment_definitions[:attachment][:s3_permissions] = :private
15 | attachment_definitions[:attachment][:s3_headers] = { content_disposition: 'attachment' }
16 | end
17 |
18 | private
19 |
20 | def prepare_drm_mark
21 | # TODO: implement DRM functionality, set new file for DRM record
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/config/locales/it.yml:
--------------------------------------------------------------------------------
1 | it:
2 | broken_file: Attenzione! questo file sembra essere rotto
3 | current_file: File Corrente
4 | delete_file: Elimina questo file
5 | delete_file_confirmation: Sei sicuro di voler eliminare il file %{filename}?
6 | digital_delivery: Consegna Digitale
7 | digital_download: Download %{filename} ↓
8 | digital_download_links: Link al Download Digitale
9 | file_is_not_ready: File is processing, please stand by
10 | digital_format:
11 | mp3: Audio MP3
12 | mobi: Kindle eBook
13 | epub: ePub eBook
14 | pdf: pdf eBook
15 | digital_versions: Versioni Digitali
16 | new_file: Nuovo File
17 | spree:
18 | digitals:
19 | unauthorized:
20 | explained: This download link has expired.
21 | unauthorized: Unauthorized
22 | solidus_digital:
23 | upload: Upload
24 |
--------------------------------------------------------------------------------
/db/migrate/20110410134726_create_digitals.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateDigitals < ActiveRecord::Migration[4.2]
4 | def self.up
5 | create_table :digitals do |t|
6 | t.integer :variant_id
7 | t.string :attachment_file_name
8 | t.string :attachment_content_type
9 | t.integer :attachment_file_size
10 | t.timestamps
11 | end
12 | add_index :digitals, :variant_id
13 |
14 | create_table :digital_links, force: true do |t|
15 | t.integer :digital_id
16 | t.integer :line_item_id
17 | t.string :secret
18 | t.integer :access_counter
19 | t.timestamps
20 | end
21 | add_index :digital_links, :digital_id
22 | add_index :digital_links, :line_item_id
23 | add_index :digital_links, :secret
24 | end
25 |
26 | def self.down
27 | drop_table :digitals
28 | drop_table :digital_links
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/solidus_digital/testing_support/factories.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :digital, class: Spree::Digital do |f|
5 | f.variant { |p| p.association(:variant) }
6 | f.attachment_content_type { 'application/octet-stream' }
7 | f.attachment_file_name { "#{SecureRandom.hex(5)}.epub" }
8 | end
9 |
10 | factory :digital_link, class: Spree::DigitalLink do |f|
11 | f.digital { |p| p.association(:digital) }
12 | f.line_item { |p| p.association(:line_item) }
13 | end
14 |
15 | factory :digital_shipping_calculator, class: Spree::Calculator::Shipping::DigitalDelivery do
16 | after :create do |c|
17 | c.set_preference(:amount, 0)
18 | end
19 | end
20 |
21 | factory :digital_shipping_method, parent: :shipping_method do
22 | name { "Digital Delivery" }
23 | calculator { FactoryBot.build :digital_shipping_calculator }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/generators/solidus_digital/install/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Generators
5 | class InstallGenerator < Rails::Generators::Base
6 | class_option :auto_run_migrations, type: :boolean, default: false
7 |
8 | def self.exit_on_failure?
9 | true
10 | end
11 |
12 | def add_migrations
13 | run 'bin/rails railties:install:migrations FROM=solidus_digital'
14 | end
15 |
16 | def run_migrations
17 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Layout/LineLength
18 | if run_migrations
19 | run 'bin/rails db:migrate'
20 | else
21 | puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_digital/spree/variant_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Spree
5 | module VariantDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | has_many :digitals
9 | after_save :destroy_digital, if: :deleted?
10 | end
11 | end
12 |
13 | # Is this variant to be downloaded by the customer?
14 | def digital?
15 | digitals.present?
16 | end
17 |
18 | private
19 |
20 | # :dependent => :destroy needs to be handled manually
21 | # spree does not delete variants, just marks them as deleted?
22 | # optionally keep digitals around for customers who require continued access to their purchases
23 | def destroy_digital
24 | digitals.map(&:destroy) unless ::Spree::DigitalConfiguration[:keep_digitals]
25 | end
26 |
27 | ::Spree::Variant.prepend self
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/spree/admin/digitals_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | module Admin
5 | class DigitalsController < ResourceController
6 | belongs_to "spree/product", find_by: :slug
7 |
8 | def create
9 | invoke_callbacks(:create, :before)
10 | @object.attributes = permitted_resource_params
11 |
12 | if @object.valid?
13 | super
14 | else
15 | invoke_callbacks(:create, :fails)
16 | flash[:error] = @object.errors.full_messages.join(", ")
17 | redirect_to location_after_save
18 | end
19 | end
20 |
21 | protected
22 |
23 | def location_after_save
24 | spree.admin_product_digitals_path(@product)
25 | end
26 |
27 | def permitted_resource_params
28 | params.require(:digital).permit(permitted_digital_attributes)
29 | end
30 |
31 | def permitted_digital_attributes
32 | [:variant_id, :drm, :attachment]
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/controllers/admin/orders_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Admin::OrdersController do
6 | context "with authorization" do
7 | stub_authorization!
8 |
9 | let(:order) do
10 | create(:completed_order_with_totals) do |o|
11 | create(:digital, variant: o.line_items.first.variant)
12 | end
13 | end
14 | let!(:digital_link) do
15 | create(:digital_link, access_counter: 3, line_item: order.line_items.first)
16 | end
17 |
18 | before do
19 | request.env["HTTP_REFERER"] = "http://localhost:3000"
20 | end
21 |
22 | describe '#reset_digitals' do
23 | it 'resets digitals for an order' do
24 | expect do
25 | get :reset_digitals, params: { id: order.number }
26 | digital_link.reload
27 | end.to change(digital_link, :access_counter).to(0)
28 |
29 | expect(response).to redirect_to(spree.edit_admin_order_path(order))
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/models/spree/stock/splitter/digital_splitter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | module Stock
5 | module Splitter
6 | class DigitalSplitter < Spree::Stock::Splitter::Base
7 | def split(packages)
8 | split_packages = []
9 | packages.each do |package|
10 | split_packages += split_by_digital(package)
11 | end
12 | return_next split_packages
13 | end
14 |
15 | private
16 |
17 | def split_by_digital(package)
18 | digitals = Hash.new { |hash, key| hash[key] = [] }
19 | package.contents.each do |item|
20 | digitals[item.variant.digital?] << item
21 | end
22 | hash_to_packages(digitals)
23 | end
24 |
25 | def hash_to_packages(digitals)
26 | packages = []
27 | digitals.each_value do |contents|
28 | packages << build_package(contents)
29 | end
30 | packages
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_digital/spree/order_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusDigital
4 | module Spree
5 | module OrderDecorator
6 | def self.prepended(base)
7 | base.state_machine.after_transition to: :complete, do: :generate_digital_links, if: :some_digital?
8 | end
9 |
10 | # all products are digital
11 | def digital?
12 | line_items.all?(&:digital?)
13 | end
14 |
15 | def some_digital?
16 | line_items.any?(&:digital?)
17 | end
18 |
19 | def digital_line_items
20 | line_items.select(&:digital?)
21 | end
22 |
23 | def digital_links
24 | digital_line_items.map(&:digital_links).flatten
25 | end
26 |
27 | def reset_digital_links!
28 | digital_links.each(&:reset!)
29 | end
30 |
31 | private
32 |
33 | def generate_digital_links
34 | line_items.each { |li| li.create_digital_links if li.digital? }
35 | end
36 |
37 | ::Spree::Order.prepend self
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/services/spree/digital_links_creator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Spree::DigitalLinksCreator
4 | attr_reader :line_item
5 |
6 | delegate :quantity, :digital_links, :variant, :master, to: :line_item
7 |
8 | def initialize(line_item)
9 | @line_item = line_item
10 | end
11 |
12 | def create_digital_links
13 | digital_links.delete_all
14 |
15 | # include master variant digitals
16 | master = variant.product.master
17 | create_digital_links_for_variant(master) if master.digital?
18 | create_digital_links_for_variant(variant) unless variant.is_master
19 | end
20 |
21 | private
22 |
23 | def create_digital_links_for_variant(variant)
24 | variant.digitals.each do |digital|
25 | digital.create_drm_record(line_item) if digital.drm?
26 | digital_links_count.times { digital_links.create!(digital: digital) }
27 | end
28 | end
29 |
30 | def digital_links_count
31 | count = Spree::DigitalConfiguration[:digital_links_count]
32 | count == 'quantity' ? quantity : Integer(count).abs
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/spree/solidus_digital_configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spree/preferences/configuration'
4 |
5 | module Spree
6 | class SpreeDigitalConfiguration < Preferences::Configuration
7 | # number of times a customer can download a digital file
8 | # nil - infinite number of clicks
9 | preference :authorized_clicks, :integer, default: 3
10 |
11 | # number of days after initial purchase the customer can download a file
12 | preference :authorized_days, :integer, default: 2
13 |
14 | # should digitals be kept around after the associated product is destroyed
15 | preference :keep_digitals, :boolean, default: false
16 |
17 | # number of seconds before an s3 link expires
18 | preference :s3_expiration_seconds, :integer, default: 10
19 |
20 | # number of digital links generated per line item
21 | # accepts: 'quantity' or Integer numbers
22 | # quantity - 'line_item.quantity' digital links will be generated
23 | # Integer 'number' - 'number' digital links will be generated
24 | preference :digital_links_count, :string, default: 'quantity'
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011-2015 halo.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/locales/de.yml:
--------------------------------------------------------------------------------
1 | de:
2 | spree:
3 | controller:
4 | controller_name: Digitale Produkte
5 | digitals:
6 | broken_file: Achtung! Diese Datei ist korrupt
7 | current_file: Aktuelle Datei
8 | delete_file: Diese Datei löschen
9 | delete_file_confirmation: Sind Sie sicher, dass die Datei %{filename} gelöscht werden soll?
10 | digitals: Digitale Produkte
11 | digital_delivery: Digitale Auslieferung
12 | digital_download: Download %{filename} ↓
13 | digital_download_links: Digitale Download Links
14 | file_is_not_ready: File is processing, please stand by
15 | digital_format:
16 | mp3: Audio MP3
17 | mobi: Kindle eBook
18 | epub: ePub eBook
19 | pdf: PDF eBook
20 | zip: ZIP-Datei
21 | digital_versions: Digitale Versionen
22 | downloads_reset: Digitale Downloads zurücksetzen
23 | files: Dateien
24 | new_file: Neue Datei
25 | reset_downloads: Digitale Downloads zurücksetzen
26 | unauthorized:
27 | explained: Der Download-Link ist nicht mehr gültig.
28 | unauthorized: Fehlende Berechtigung
29 | upload: Upload
30 |
--------------------------------------------------------------------------------
/app/controllers/spree/digitals_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | class DigitalsController < Spree::BaseController
5 | rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found
6 | before_action :authorize_digital_link, only: :show
7 |
8 | def show
9 | if digital_link.cloud?
10 | redirect_to attachment.expiring_url(Spree::DigitalConfiguration[:s3_expiration_seconds])
11 | else
12 | send_file attachment.path, filename: attachment.original_filename, type: attachment.content_type
13 | end
14 | end
15 |
16 | private
17 |
18 | def authorize_digital_link
19 | # don't authorize the link unless the file exists
20 | raise ActiveRecord::RecordNotFound if attachment.blank?
21 |
22 | render :unauthorized unless digital_link.file_exists? && digital_link.authorize!
23 | end
24 |
25 | def digital_link
26 | @link ||= DigitalLink.find_by!(secret: params[:secret])
27 | end
28 |
29 | def attachment
30 | @attachment ||= digital_link.attachment
31 | end
32 |
33 | def resource_not_found
34 | render body: nil, status: :not_found
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/config/locales/es.yml:
--------------------------------------------------------------------------------
1 | es:
2 | spree:
3 | digitals:
4 | broken_file: ¡Atención! Este archivo está corrupto
5 | current_file: Archivo actual
6 | delete_file: Borrar este archivo
7 | delete_file_confirmation: ¿Está seguro de que quiere borrar este fichero %{filename}?
8 | digital_delivery: Entrega digital
9 | digital_download: Descarga %{filename}
10 | digital_download_links: Enlaces para la descaga digital
11 | file_is_not_ready: File is processing, please stand by
12 | digital_format:
13 | mp3: Audio MP3
14 | mobi: Kindle eBook
15 | epub: ePub eBook
16 | pdf: pdf eBook
17 | digital_versions: Versiones Digitales
18 | downloads_reset: Reset de descargas digitales
19 | files: Archivos
20 | file_name: Nombre de Archivo
21 | new_file: Nuevo Archivo
22 | reset_downloads: Resetear descargas digitales
23 | unauthorized:
24 | explained: El enlace para la descarga ha expirado
25 | unauthorized: No estás autorizado
26 | upload: Subir
27 | product_no_variants: Este Producto no tiene variantes
28 | product_with_variants: "Este producto tiene versión digital:"
29 |
--------------------------------------------------------------------------------
/spec/models/spree/drm_record_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | class SampleDrmMaker
6 | def initialize(drm_record)
7 | @drm_record = drm_record
8 | end
9 |
10 | def create!
11 | if @drm_record.digital.attachment.exists?
12 | @drm_record.attachment = @drm_record.digital.attachment
13 | end
14 | end
15 | end
16 |
17 | Spree::DrmRecord.class_eval do
18 | private
19 |
20 | def prepare_drm_mark
21 | SampleDrmMaker.new(self).create!
22 | end
23 | end
24 |
25 | RSpec.describe Spree::DrmRecord do
26 | describe "#create" do
27 | let(:sample_file) { File.open(SolidusDigital::Engine.root.join("spec", "fixtures", "thinking-cat.jpg")) }
28 | let(:digital) { create(:digital, drm: true, attachment: sample_file) }
29 | let(:digital_variant) { create(:variant, digitals: [digital]) }
30 | let(:line_item) { create(:line_item, variant: digital_variant) }
31 |
32 | it "creates drm marked attachment" do
33 | drm_record = digital.drm_records.create(line_item: line_item)
34 |
35 | expect(drm_record.attachment.present?).to eq(true)
36 | expect(drm_record.attachment_file_name).to eq("thinking-cat.jpg")
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/solidus_digital/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'solidus_core'
4 | require 'solidus_support'
5 |
6 | module SolidusDigital
7 | class Engine < Rails::Engine
8 | include SolidusSupport::EngineExtensions
9 |
10 | isolate_namespace ::Spree
11 |
12 | engine_name 'solidus_digital'
13 |
14 | initializer "solidus_digital.zeitwerk_ignore_deface_overrides", before: :eager_load! do |app|
15 | app.autoloaders.main.ignore(root.join('app/overrides'))
16 | end
17 |
18 | initializer "solidus_digital.preferences", before: "spree.environment" do |_app|
19 | ::Spree::DigitalConfiguration = ::Spree::SpreeDigitalConfiguration.new
20 | end
21 |
22 | initializer "solidus_digital.digital_shipping", after: "spree.environment" do |app|
23 | app.config.spree.calculators.shipping_methods << "Spree::Calculator::Shipping::DigitalDelivery"
24 | end
25 |
26 | initializer "solidus_digital.digital_splitter", after: "spree.environment" do |app|
27 | app.config.spree.stock_splitters << "Spree::Stock::Splitter::DigitalSplitter"
28 | end
29 |
30 | # use rspec for tests
31 | config.generators do |g|
32 | g.test_framework :rspec
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | spree:
3 | controller:
4 | controller_name: Digitals
5 | digitals:
6 | broken_file: Warning! this file is broken
7 | current_file: Current File
8 | delete_file: Delete this file
9 | delete_file_confirmation: Are you sure you want to delete the file %{filename}?
10 | digitals: Digitals
11 | digital_delivery: Digital Delivery
12 | digital_download: Download %{filename} ↓
13 | digital_download_links: Digital Download Links
14 | file_is_not_ready: File is processing, please stand by
15 | digital_format:
16 | mp3: Audio MP3
17 | mobi: Kindle eBook
18 | epub: ePub eBook
19 | pdf: pdf eBook
20 | zip: ZIP file
21 | digital_versions: Digital Versions
22 | downloads_reset: Digital Downloads Reset
23 | files: Files
24 | new_file: New File
25 | reset_downloads: Reset Digital Downloads
26 | unauthorized:
27 | explained: This download link has expired.
28 | unauthorized: Unauthorized
29 | upload: Upload
30 | file_name: File name
31 | product_no_variants: This product has no variants.
32 | product_with_variants: "A digital version of this product currently exists:"
33 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Configure Rails Environment
4 | ENV['RAILS_ENV'] = 'test'
5 |
6 | # Run Coverage report
7 | require 'solidus_dev_support/rspec/coverage'
8 |
9 | # Create the dummy app if it's still missing.
10 | dummy_env = "#{__dir__}/dummy/config/environment.rb"
11 | system 'bin/rake extension:test_app' unless File.exist? dummy_env
12 | require dummy_env
13 |
14 | # Requires factories and other useful helpers defined in spree_core.
15 | require 'solidus_dev_support/rspec/feature_helper'
16 |
17 | # Requires supporting ruby files with custom matchers and macros, etc,
18 | # in spec/support/ and its subdirectories.
19 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
20 |
21 | # Requires factories defined in Solidus core and this extension.
22 | # See: lib/solidus_digital/testing_support/factories.rb
23 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusDigital::Engine)
24 |
25 | RSpec.configure do |config|
26 | config.infer_spec_type_from_file_location!
27 | config.use_transactional_fixtures = false
28 | config.include Devise::Test::ControllerHelpers, type: :controller
29 |
30 | if Spree.solidus_gem_version < Gem::Version.new('2.11')
31 | config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :system
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/models/spree/stock/splitter/digital_splitter_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | module Stock
7 | module Splitter
8 | RSpec.describe DigitalSplitter do
9 | subject { described_class.new(stock_location) }
10 |
11 | let(:item1) { create(:inventory_unit, variant: create(:digital).variant) }
12 | let(:item2) { create(:inventory_unit, variant: create(:variant)) }
13 | let(:item3) { create(:inventory_unit, variant: create(:variant)) }
14 | let(:item4) { create(:inventory_unit, variant: create(:digital).variant) }
15 | let(:item5) { create(:inventory_unit, variant: create(:digital).variant) }
16 |
17 | let(:stock_location) { mock_model(Spree::StockLocation) }
18 |
19 | it 'splits each package by product' do
20 | package1 = Package.new(stock_location)
21 | package1.add item1, :on_hand
22 | package1.add item2, :on_hand
23 | package1.add item3, :on_hand
24 | package1.add item4, :on_hand
25 | package1.add item5, :on_hand
26 |
27 | packages = subject.split([package1])
28 |
29 | expect(packages[0].quantity).to eq 3
30 | expect(packages[1].quantity).to eq 2
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/models/spree/variant_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Variant do
6 | describe "#destroy" do
7 | let(:variant) { create(:variant) }
8 | let!(:digital) { create(:digital, variant: variant) }
9 |
10 | it "destroys associated digitals by default" do
11 | # default is false
12 | stub_spree_preferences(Spree::DigitalConfiguration, keep_digitals: false)
13 |
14 | expect(Spree::Digital.count).to eq(1)
15 | expect(variant.digitals.present?).to be true
16 |
17 | variant.deleted_at = Time.zone.now
18 | expect(variant.deleted?).to be true
19 | variant.save!
20 |
21 | expect { digital.reload.present? }.to raise_error(ActiveRecord::RecordNotFound)
22 | expect(Spree::Digital.count).to eq(0)
23 | end
24 |
25 | it "conditionallies keep associated digitals" do
26 | stub_spree_preferences(Spree::DigitalConfiguration, keep_digitals: true)
27 |
28 | expect(Spree::Digital.count).to eq(1)
29 | expect(variant.digitals.present?).to be true
30 |
31 | variant.deleted_at = Time.zone.now
32 | variant.save!
33 | expect(variant.deleted?).to be true
34 | expect { digital.reload.present? }.not_to raise_error
35 | expect(Spree::Digital.count).to eq(1)
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/solidus_digital.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/solidus_digital/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'solidus_digital'
7 | spec.version = SolidusDigital::VERSION
8 | spec.summary = 'Digital download functionality for Solidus'
9 | spec.description = spec.summary
10 | spec.license = 'BSD-3-Clause'
11 |
12 | spec.author = ['funkensturm', 'Michael Bianco']
13 | spec.email = 'info@cliffsidedev.com'
14 | spec.homepage = 'https://github.com/solidusio-contrib/solidus_digital'
15 |
16 | if spec.respond_to?(:metadata)
17 | spec.metadata["homepage_uri"] = spec.homepage if spec.homepage
18 | spec.metadata["source_code_uri"] = spec.homepage if spec.homepage
19 | end
20 |
21 | spec.required_ruby_version = ['>= 2.4', '< 4.0']
22 |
23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25 | end
26 | spec.test_files = Dir['spec/**/*']
27 | spec.bindir = "exe"
28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29 | spec.require_paths = ["lib"]
30 |
31 | spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 5']
32 | spec.add_dependency 'solidus_support', '~> 0.5'
33 |
34 | spec.add_development_dependency 'rspec-activemodel-mocks'
35 | spec.add_development_dependency 'shoulda-matchers'
36 | spec.add_development_dependency 'solidus_dev_support'
37 | end
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 funkensturm
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name Solidus nor the names of its contributors may be used to
13 | endorse or promote products derived from this software without specific
14 | prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/app/views/spree/order_mailer/confirm_email.text.erb:
--------------------------------------------------------------------------------
1 | <%= I18n.t('spree.order_mailer.confirm_email.dear_customer') %>
2 |
3 | <%= I18n.t('spree.order_mailer.confirm_email.instructions') %>
4 |
5 | ============================================================
6 | <%= I18n.t('spree.order_mailer.confirm_email.order_summary') %>
7 | ============================================================
8 |
9 | <% @order.line_items.each do |item| %>
10 | <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %>
11 | <% end %>
12 | ============================================================
13 | <%= I18n.t('spree.order_mailer.confirm_email.subtotal', :subtotal => @order.display_item_total) %>
14 |
15 | <% @order.adjustments.eligible.each do |adjustment| %>
16 | <%= raw(adjustment.label) %> <%= adjustment.display_amount %>
17 | <% end %>
18 |
19 | <%= I18n.t('spree.order_mailer.confirm_email.total', :total => @order.display_total) %>
20 |
21 | <% if @order.some_digital? %>
22 | ============================================================
23 | Download links for digital products
24 | ============================================================
25 |
26 | ATTENTION! Each link will only work a SINGLE TIME!
27 | Also, they will only work WITHIN 24 HOURS!
28 |
29 | <% for item in @order.line_items %>
30 | <% if item.digital? %>
31 | <%= raw item.variant.name %>:
32 | <% for link in item.digital_links %>
33 | <%= digital_url :host => @order.store.url, :secret => link.secret %>
34 | <% end %>
35 | <% end %>
36 | <% end %>
37 | ============================================================
38 | <% end %>
39 |
40 | <%= I18n.t('spree.order_mailer.confirm_email.thanks') %>
41 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | branch = ENV.fetch('SOLIDUS_BRANCH', 'main')
7 | gem 'solidus', github: 'solidusio/solidus', branch: branch
8 |
9 | # The solidus_frontend gem has been pulled out since v3.2
10 | if branch >= 'v3.2'
11 | gem 'solidus_frontend'
12 | elsif branch == 'main'
13 | gem 'solidus_frontend', github: 'solidusio/solidus_frontend'
14 | else
15 | gem 'solidus_frontend', github: 'solidusio/solidus', branch: branch
16 | end
17 |
18 | # Needed to help Bundler figure out how to resolve dependencies,
19 | # otherwise it takes forever to resolve them.
20 | # See https://github.com/bundler/bundler/issues/6677
21 | gem 'rails', '>0.a'
22 |
23 | # Provides basic authentication functionality for testing parts of your engine
24 | gem 'solidus_auth_devise'
25 |
26 | case ENV.fetch('DB', nil)
27 | when 'mysql'
28 | gem 'mysql2'
29 | when 'postgresql'
30 | gem 'pg'
31 | else
32 | gem 'sqlite3', '~> 1.4'
33 | end
34 |
35 | gem 'rails-controller-testing'
36 |
37 | # While we still support Ruby < 3 we need to workaround a limitation in
38 | # the 'async' gem that relies on the latest ruby, since RubyGems doesn't
39 | # resolve gems based on the required ruby version.
40 | gem 'async', '< 3' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3')
41 |
42 | gemspec
43 |
44 | # Use a local Gemfile to include development dependencies that might not be
45 | # relevant for the project or for other contributors, e.g. pry-byebug.
46 | #
47 | # We use `send` instead of calling `eval_gemfile` to work around an issue with
48 | # how Dependabot parses projects: https://github.com/dependabot/dependabot-core/issues/1658.
49 | send(:eval_gemfile, 'Gemfile-local') if File.exist? 'Gemfile-local'
50 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | # Required for feature specs.
5 | browser-tools: circleci/browser-tools@1.1
6 |
7 | # Always take the latest version of the orb, this allows us to
8 | # run specs against Solidus supported versions only without the need
9 | # to change this configuration every time a Solidus version is released
10 | # or goes EOL.
11 | solidusio_extensions: solidusio/extensions@volatile
12 |
13 | jobs:
14 | run-specs-with-sqlite:
15 | executor:
16 | name: solidusio_extensions/sqlite
17 | ruby_version: "3.2"
18 | steps:
19 | - browser-tools/install-chrome
20 | - solidusio_extensions/run-tests
21 | run-specs-with-postgres:
22 | executor:
23 | name: solidusio_extensions/postgres
24 | ruby_version: "3.2"
25 | steps:
26 | - browser-tools/install-chrome
27 | - solidusio_extensions/run-tests
28 | run-specs-with-mysql:
29 | executor:
30 | name: solidusio_extensions/mysql
31 | ruby_version: "3.2"
32 | steps:
33 | - browser-tools/install-chrome
34 | - solidusio_extensions/run-tests
35 | lint-code:
36 | executor:
37 | name: solidusio_extensions/sqlite-memory
38 | ruby_version: "3.2"
39 | steps:
40 | - solidusio_extensions/lint-code
41 |
42 | workflows:
43 | "Run specs on supported Solidus versions":
44 | jobs:
45 | - run-specs-with-sqlite
46 | - run-specs-with-postgres
47 | - run-specs-with-mysql
48 | - lint-code
49 |
50 | "Weekly run specs against main":
51 | triggers:
52 | - schedule:
53 | cron: "0 0 * * 4" # every Thursday
54 | filters:
55 | branches:
56 | only:
57 | - main
58 | jobs:
59 | - run-specs-with-sqlite
60 | - run-specs-with-postgres
61 | - run-specs-with-mysql
62 |
--------------------------------------------------------------------------------
/app/models/spree/digital_link.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | class DigitalLink < ApplicationRecord
5 | belongs_to :digital
6 | belongs_to :line_item
7 |
8 | validates :digital, presence: true
9 | validates :secret, length: { is: 30 }
10 | before_validation :set_defaults, on: :create
11 |
12 | delegate :attachment_file_name, :cloud?, to: :digital
13 |
14 | # Can this link still be used? It is valid if it's less than 24 hours old and was not accessed more than 3 times
15 | def authorizable?
16 | !(expired? || access_limit_exceeded?)
17 | end
18 |
19 | def expired?
20 | created_at <= Spree::DigitalConfiguration[:authorized_days].day.ago
21 | end
22 |
23 | def ready?
24 | attachment.exists?
25 | end
26 |
27 | def file_exists?
28 | cloud? ? attachment.exists? : File.file?(attachment.path)
29 | end
30 |
31 | def access_limit_exceeded?
32 | return false if Spree::DigitalConfiguration[:authorized_clicks].nil?
33 |
34 | access_counter >= Spree::DigitalConfiguration[:authorized_clicks]
35 | end
36 |
37 | # This method should be called when a download is initiated.
38 | # It returns +true+ or +false+ depending on whether the authorization is granted.
39 | def authorize!
40 | authorizable? && increment!(:access_counter) ? true : false
41 | end
42 |
43 | def reset!
44 | update_column :access_counter, 0
45 | update_column :created_at, Time.zone.now
46 | end
47 |
48 | def attachment
49 | if digital.drm?
50 | digital.drm_records.find_by(line_item: line_item).attachment
51 | else
52 | digital.attachment
53 | end
54 | end
55 |
56 | private
57 |
58 | # Populating the secret automatically and zero'ing the access_counter (otherwise it might turn out to be NULL)
59 | def set_defaults
60 | self.secret = SecureRandom.hex(15)
61 | self.access_counter ||= 0
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/models/calculator/digital_delivery_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Calculator::Shipping::DigitalDelivery do
6 | subject { described_class.new }
7 |
8 | it 'has a description for the class' do
9 | expect(described_class).to respond_to(:description)
10 | end
11 |
12 | describe '#compute_package' do
13 | it 'ignores the passed in object' do
14 | expect {
15 | subject.compute_package(double)
16 | }.not_to raise_error
17 | end
18 |
19 | it 'alwayses return the preferred_amount' do
20 | amount_double = double
21 | expect(subject).to receive(:preferred_amount).and_return(amount_double)
22 | expect(subject.compute_package(double)).to eq(amount_double)
23 | end
24 | end
25 |
26 | describe '#available?' do
27 | let(:digital_variant) { build(:variant, digitals: [build(:digital)]) }
28 | let(:regular_variant) { build(:variant) }
29 |
30 | let(:digital_order) {
31 | package = Spree::Stock::Package.new(build(:stock_location), [])
32 | package.add(build(:inventory_unit, variant: digital_variant))
33 | package
34 | }
35 |
36 | let(:mixed_order) {
37 | package = Spree::Stock::Package.new(build(:stock_location), [])
38 | package.add(build(:inventory_unit, variant: digital_variant))
39 | package.add(build(:inventory_unit, variant: regular_variant))
40 | package
41 | }
42 |
43 | let(:non_digital_order) {
44 | package = Spree::Stock::Package.new(build(:stock_location), [])
45 | package.add(build(:inventory_unit, variant: regular_variant))
46 | package
47 | }
48 |
49 | it 'returns true for a digital order' do
50 | expect(subject.available?(digital_order)).to be true
51 | end
52 |
53 | it 'returns false for a mixed order' do
54 | expect(subject.available?(mixed_order)).to be false
55 | end
56 |
57 | it 'returns false for an exclusively non-digital order' do
58 | expect(subject.available?(non_digital_order)).to be false
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/app/views/spree/admin/digitals/_form.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= form_for(:digital, :url => admin_product_digitals_path(@product), :method => :create, :multipart => true ) do |f| %>
3 |
47 | <% end %>
48 |
49 |
--------------------------------------------------------------------------------
/spec/controllers/digitals_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::DigitalsController, type: :controller do
6 | describe '#show' do
7 | let(:digital) { create(:digital) }
8 | let(:digital_link) { create(:digital_link, digital: digital) }
9 |
10 | it 'returns a 404 for a non-existent secret' do
11 | get :show, params: { secret: 'NotReal00000000000000000000000' }
12 | expect(response.status).to eq(404)
13 | end
14 |
15 | context 'unauthorized' do
16 | it 'returns a 200 and calls send_file for link that is not a file' do
17 | expect(digital_link).not_to receive(:cloud?)
18 | expect(controller).not_to receive(:send_file)
19 |
20 | get :show, params: { secret: digital_link.secret }
21 | expect(response.status).to eq(200)
22 | expect(response).to render_template(:unauthorized)
23 | end
24 | end
25 |
26 | context 'authorized' do
27 | before { allow(controller).to receive(:authorize_digital_link).and_return(true) }
28 |
29 | it 'returns a 200 and calls send_file that is a file' do
30 | expect(controller)
31 | .to receive(:send_file)
32 | .with(
33 | digital.attachment.path,
34 | filename: digital.attachment.original_filename,
35 | type: digital.attachment.content_type
36 | ){ controller.render body: nil, content_type: digital.attachment.content_type }
37 |
38 | get :show, params: { secret: digital_link.secret }
39 | expect(response.status).to eq(200)
40 | expect(response.header['Content-Type']).to match digital.attachment.content_type
41 | end
42 |
43 | it 'redirects to s3 when using s3' do
44 | skip 'TODO: needs a way to test without having a bucket'
45 | Paperclip::Attachment.default_options[:storage] = :s3
46 |
47 | expect(controller).to receive(:redirect_to)
48 | expect(controller).to receive(:attachment_is_file?).and_return(true)
49 | expect(controller).not_to receive(:send_file)
50 |
51 | get :show, params: { secret: digital_link.secret }
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/bin/sandbox:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | test -z "${DEBUG+empty_string}" || set -x
5 |
6 | test "$DB" = "sqlite" && export DB="sqlite3"
7 |
8 | if [ -z "$SOLIDUS_BRANCH" ]
9 | then
10 | echo "~~> Use 'export SOLIDUS_BRANCH=[main|v3.2|...]' to control the Solidus branch"
11 | SOLIDUS_BRANCH="main"
12 | fi
13 | echo "~~> Using branch $SOLIDUS_BRANCH of solidus"
14 |
15 | if [ -z "$SOLIDUS_FRONTEND" ]
16 | then
17 | echo "~~> Use 'export SOLIDUS_FRONTEND=[solidus_frontend|solidus_starter_frontend]' to control the Solidus frontend"
18 | SOLIDUS_FRONTEND="solidus_frontend"
19 | fi
20 | echo "~~> Using branch $SOLIDUS_FRONTEND as the solidus frontend"
21 |
22 | extension_name="solidus_digital"
23 |
24 | # Stay away from the bundler env of the containing extension.
25 | function unbundled {
26 | ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
27 | }
28 |
29 | rm -rf ./sandbox
30 | unbundled bundle exec rails new sandbox \
31 | --database="${DB:-sqlite3}" \
32 | --skip-bundle \
33 | --skip-git \
34 | --skip-keeps \
35 | --skip-rc \
36 | --skip-spring \
37 | --skip-test \
38 | --skip-javascript
39 |
40 | if [ ! -d "sandbox" ]; then
41 | echo 'sandbox rails application failed'
42 | exit 1
43 | fi
44 |
45 | cd ./sandbox
46 | cat <> Gemfile
47 | gem 'solidus', github: 'solidusio/solidus', branch: '$SOLIDUS_BRANCH'
48 | gem 'rails-i18n'
49 | gem 'solidus_i18n'
50 |
51 | gem '$extension_name', path: '..'
52 |
53 | group :test, :development do
54 | platforms :mri do
55 | gem 'pry-byebug'
56 | end
57 | end
58 | RUBY
59 |
60 | unbundled bundle install --gemfile Gemfile
61 |
62 | unbundled bundle exec rake db:drop db:create
63 |
64 | unbundled bundle exec rails generate solidus:install \
65 | --auto-accept \
66 | --user_class=Spree::User \
67 | --enforce_available_locales=true \
68 | --with-authentication=true \
69 | --payment-method=none \
70 | --frontend=${SOLIDUS_FRONTEND} \
71 | $@
72 |
73 | unbundled bundle exec rails generate solidus:auth:install --auto-run-migrations
74 | unbundled bundle exec rails generate ${extension_name}:install --auto-run-migrations
75 |
76 | echo
77 | echo "🚀 Sandbox app successfully created for $extension_name!"
78 | echo "🧪 This app is intended for test purposes."
79 |
--------------------------------------------------------------------------------
/spec/models/spree/line_item_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'spree/testing_support/order_walkthrough'
5 |
6 | RSpec.describe Spree::LineItem do
7 | let(:order) { create(:order) }
8 | let(:digital) { create(:digital) }
9 | let!(:digital_variant) { create(:variant, digitals: [digital]) }
10 | let!(:master_digital_variant) { create(:on_demand_master_variant, digitals: [create(:digital)]) }
11 |
12 | describe "#digital?" do
13 | it "reports as digital if either the master variant or selected variant has digitals" do
14 | expect(build(:variant)).not_to be_digital
15 | expect(build(:on_demand_master_variant)).not_to be_digital
16 |
17 | expect(digital_variant).to be_digital
18 | expect(master_digital_variant).to be_digital
19 | end
20 | end
21 |
22 | describe "#create_digital_links" do
23 | let(:line_item) { create(:line_item, order: order, variant: digital_variant) }
24 |
25 | context "digital has drm restrictions" do
26 | let(:digital) { create(:digital, drm: true) }
27 |
28 | it "creates digital link to drm record" do
29 | line_item.create_digital_links
30 |
31 | expect(digital.drm_records.count).to eq(1)
32 | end
33 | end
34 |
35 | context "digital links" do
36 | let(:digital) { create(:digital) }
37 |
38 | before do
39 | line_item.quantity = 8
40 | line_item.save
41 | end
42 |
43 | after { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "quantity") }
44 |
45 | context "when :digital_links_count settings set to 'quantity' (default)" do
46 | it "generates quantity x count digital links " do
47 | line_item.create_digital_links
48 |
49 | expect(digital.digital_links.count).to eq(8)
50 | end
51 | end
52 |
53 | context "when :digital_links_count settings set to '1'" do
54 | before { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "1") }
55 |
56 | it "generates quantity x count digital links " do
57 | line_item.create_digital_links
58 |
59 | expect(digital.digital_links.count).to eq(1)
60 | end
61 | end
62 |
63 | context "when :digital_links_count settings set to '-1'" do
64 | before { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "-1") }
65 |
66 | it "generates quantity x count digital links " do
67 | line_item.create_digital_links
68 |
69 | expect(digital.digital_links.count).to eq(1)
70 | end
71 | end
72 | end
73 | end
74 |
75 | describe "#destroy" do
76 | it "destroys associated links when destroyed" do
77 | line_item = create(:line_item, order: order, variant: digital_variant)
78 | line_item.create_digital_links
79 | links = line_item.digital_links
80 |
81 | expect(links.to_a.size).to eq(1)
82 | expect(links.first.line_item).to eq(line_item)
83 | expect {
84 | line_item.destroy
85 | }.to change(Spree::DigitalLink, :count).by(-1)
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/spec/models/spree/digital_link_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::DigitalLink do
6 | context 'validation' do
7 | it { is_expected.to belong_to(:digital) }
8 | it { is_expected.to belong_to(:line_item) }
9 | end
10 |
11 | describe "#create" do
12 | it "creates an appropriately long secret" do
13 | expect(create(:digital_link, secret: nil).secret.length).to eq(30)
14 | end
15 |
16 | it "zeroes out the access counter on creation" do
17 | expect(create(:digital_link, access_counter: nil).access_counter).to eq(0)
18 | end
19 | end
20 |
21 | describe "#update" do
22 | it "does not change the secret when updated" do
23 | digital_link = create(:digital_link)
24 | secret = digital_link.secret
25 | digital_link.increment(:access_counter).save
26 | expect(digital_link.secret).to eq(secret)
27 | end
28 |
29 | it "enforces to have an associated digital" do
30 | link = create(:digital_link)
31 | expect { link.update!(digital: nil) }.to raise_error(ActiveRecord::RecordInvalid)
32 | end
33 |
34 | it "does not allow an empty or too short secret" do
35 | link = create(:digital_link)
36 | expect { link.update!(secret: nil) }.to raise_error(ActiveRecord::RecordInvalid)
37 | expect { link.update!(secret: 'x' * 25) }.to raise_error(ActiveRecord::RecordInvalid)
38 | end
39 | end
40 |
41 | context "authorization" do
42 | let(:link) { create(:digital_link) }
43 |
44 | before { Spree::DigitalConfiguration.reset }
45 |
46 | it "increments the counter using #authorize!" do
47 | expect(link.access_counter).to eq(0)
48 | expect { link.authorize! }.to change(link, :access_counter).by(1)
49 | end
50 |
51 | it "is #authorized? when configuration for access_counter set to nil" do
52 | stub_spree_preferences(Spree::DigitalConfiguration, authorized_clicks: nil)
53 | expect(link.authorizable?).to be true
54 | end
55 |
56 | it "is not #authorized? when the access_counter is too high" do
57 | allow(link).to receive_messages(access_counter: Spree::DigitalConfiguration[:authorized_clicks] - 1)
58 | expect(link.authorizable?).to be true
59 |
60 | allow(link).to receive_messages(access_counter: Spree::DigitalConfiguration[:authorized_clicks])
61 | expect(link.authorizable?).to be false
62 | end
63 |
64 | it "is not #authorize! when the created_at date is too far in the past" do
65 | expect(link.authorize!).to be true
66 |
67 | allow(link).to receive_messages(created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 - 1).hours.ago)
68 | expect(link.authorize!).to be true
69 |
70 | allow(link).to receive_messages(created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago)
71 | expect(link.authorize!).to be false
72 | end
73 |
74 | it "is not #authorized? when both access_counter and created_at are invalid" do
75 | expect(link.authorizable?).to be true
76 | allow(link).to receive_messages(
77 | access_counter: Spree::DigitalConfiguration[:authorized_clicks],
78 | created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago
79 | )
80 | expect(link.authorizable?).to be false
81 | end
82 | end
83 |
84 | describe '#reset!' do
85 | it 'resets the access counter' do
86 | link = create(:digital_link)
87 | link.authorize!
88 | expect(link.access_counter).to eq(1)
89 |
90 | link.reset!
91 | expect(link.access_counter).to eq(0)
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/controllers/admin/digitals_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe Spree::Admin::DigitalsController do
6 | stub_authorization!
7 |
8 | let!(:product) { create(:product) }
9 |
10 | describe '#index' do
11 | render_views
12 |
13 | context "with variants" do
14 | let(:digitals) { 3.times.map { create(:digital) } }
15 | let(:variants_with_digitals) do
16 | digitals.map { |d| create(:variant, product: product, digitals: [d]) }
17 | end
18 | let(:variants_without_digitals) { 3.times.map { create(:variant, product: product) } }
19 |
20 | it "displays an empty page when no digitals exist" do
21 | variants_without_digitals
22 | get :index, params: { product_id: product.slug }
23 | end
24 |
25 | it "displays list of digitals when they exist" do
26 | variants_with_digitals
27 | get :index, params: { product_id: product.slug }
28 |
29 | variants_with_digitals.each do |variant|
30 | variant.digitals.each do |digital|
31 | expect(response.body).to include(digital.attachment_file_name)
32 | end
33 | end
34 | end
35 | end
36 |
37 | context "without non-master variants" do
38 | it "displays an empty page when the master variant is not digital" do
39 | get :index, params: { product_id: product.slug }
40 | expect(response.code).to eq("200")
41 | expect(response.body).to include("This product has no variants.")
42 | expect(response.body).not_to include('A digital version of this product currently exists:')
43 | end
44 |
45 | it "displays the variant details when the master is digital" do
46 | @digital = create :digital, variant: product.master
47 | get :index, params: { product_id: product.slug }
48 |
49 | expect(response.code).to eq("200")
50 | expect(response.body).to include('A digital version of this product currently exists:')
51 | end
52 | end
53 | end
54 |
55 | describe '#create' do
56 | context 'for a product that exists' do
57 | let!(:variant) { create(:variant, product: product) }
58 |
59 | it 'creates a digital associated with the variant and product' do
60 | expect {
61 | post :create, params: { product_id: product.slug,
62 | digital: { variant_id: variant.id,
63 | attachment: upload_image('thinking-cat.jpg') } }
64 | expect(response).to redirect_to(spree.admin_product_digitals_path(product))
65 | }.to change(Spree::Digital, :count).by(1)
66 | end
67 | end
68 |
69 | context 'for an invalid object' do
70 | it 'redirects to the index page' do
71 | expect {
72 | post :create, params: { product_id: product.slug, digital: { variant_id: product.master.id } } # fail validation by not passing attachment
73 | expect(flash[:error]).to eq "Attachment can't be blank"
74 | expect(response).to redirect_to(spree.admin_product_digitals_path(product))
75 | }.to change(Spree::Digital, :count).by(0)
76 | end
77 | end
78 | end
79 |
80 | describe '#destroy' do
81 | let(:digital) { create(:digital) }
82 | let!(:variant) { create(:variant, product: product, digitals: [digital]) }
83 |
84 | context 'for a digital and product that exist' do
85 | it 'deletes the associated digital' do
86 | expect {
87 | delete :destroy, params: { product_id: product.slug, id: digital.id }
88 | expect(response).to redirect_to(spree.admin_product_digitals_path(product))
89 | }.to change(Spree::Digital, :count).by(-1)
90 | end
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2023-06-19 16:42:10 UTC using RuboCop version 1.52.1.
4 | # The point is for the user to remove these configuration records
5 | # one by one as the offenses are removed from the code base.
6 | # Note that changes in the inspected code, or installation of new
7 | # versions of RuboCop, may require this file to be generated again.
8 |
9 | # Offense count: 17
10 | # This cop supports unsafe autocorrection (--autocorrect-all).
11 | # Configuration parameters: Include, EnforcedStyle, ExplicitOnly.
12 | # Include: **/*_spec.rb, **/spec/**/*, spec/factories.rb, spec/factories/**/*.rb, features/support/factories/**/*.rb
13 | # SupportedStyles: create_list, n_times
14 | FactoryBot/CreateList:
15 | Exclude:
16 | - 'spec/controllers/admin/digitals_controller_spec.rb'
17 | - 'spec/models/spree/line_item_spec.rb'
18 | - 'spec/models/spree/order_spec.rb'
19 | - 'spec/models/spree/product_spec.rb'
20 |
21 | # Offense count: 1
22 | # Configuration parameters: Severity, Include.
23 | # Include: **/*.gemspec
24 | Gemspec/RequiredRubyVersion:
25 | Exclude:
26 | - 'solidus_digital.gemspec'
27 |
28 | # Offense count: 1
29 | # This cop supports safe autocorrection (--autocorrect).
30 | Lint/RedundantCopDisableDirective:
31 | Exclude:
32 | - 'lib/generators/solidus_digital/install/install_generator.rb'
33 |
34 | # Offense count: 1
35 | # This cop supports unsafe autocorrection (--autocorrect-all).
36 | # Configuration parameters: EnforcedStyleForLeadingUnderscores.
37 | # SupportedStylesForLeadingUnderscores: disallowed, required, optional
38 | Naming/MemoizedInstanceVariableName:
39 | Exclude:
40 | - 'app/controllers/spree/digitals_controller.rb'
41 |
42 | # Offense count: 2
43 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
44 | # SupportedStyles: snake_case, normalcase, non_integer
45 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
46 | Naming/VariableNumber:
47 | Exclude:
48 | - 'spec/models/spree/order_spec.rb'
49 |
50 | # Offense count: 11
51 | # This cop supports unsafe autocorrection (--autocorrect-all).
52 | Performance/TimesMap:
53 | Exclude:
54 | - 'spec/controllers/admin/digitals_controller_spec.rb'
55 | - 'spec/models/spree/order_spec.rb'
56 | - 'spec/models/spree/product_spec.rb'
57 |
58 | # Offense count: 13
59 | # Configuration parameters: Prefixes, AllowedPatterns.
60 | # Prefixes: when, with, without
61 | RSpec/ContextWording:
62 | Exclude:
63 | - 'spec/controllers/admin/digitals_controller_spec.rb'
64 | - 'spec/controllers/digitals_controller_spec.rb'
65 | - 'spec/models/spree/digital_link_spec.rb'
66 | - 'spec/models/spree/digital_spec.rb'
67 | - 'spec/models/spree/line_item_spec.rb'
68 | - 'spec/models/spree/order_spec.rb'
69 | - 'spec/models/spree/product_spec.rb'
70 |
71 | # Offense count: 3
72 | RSpec/ExpectInHook:
73 | Exclude:
74 | - 'spec/models/spree/order_spec.rb'
75 |
76 | # Offense count: 4
77 | # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
78 | # Include: **/*_spec*rb*, **/spec/**/*
79 | RSpec/FilePath:
80 | Exclude:
81 | - 'spec/controllers/admin/digitals_controller_spec.rb'
82 | - 'spec/controllers/admin/orders_controller_spec.rb'
83 | - 'spec/controllers/digitals_controller_spec.rb'
84 | - 'spec/models/calculator/digital_delivery_spec.rb'
85 |
86 | # Offense count: 2
87 | RSpec/IteratedExpectation:
88 | Exclude:
89 | - 'spec/models/spree/order_spec.rb'
90 |
91 | # Offense count: 1
92 | # This cop supports safe autocorrection (--autocorrect).
93 | RSpec/LeadingSubject:
94 | Exclude:
95 | - 'spec/models/spree/order_spec.rb'
96 |
97 | # Offense count: 2
98 | RSpec/LetSetup:
99 | Exclude:
100 | - 'spec/controllers/admin/digitals_controller_spec.rb'
101 | - 'spec/models/spree/product_spec.rb'
102 |
103 | # Offense count: 10
104 | # Configuration parameters: .
105 | # SupportedStyles: have_received, receive
106 | RSpec/MessageSpies:
107 | EnforcedStyle: receive
108 |
109 | # Offense count: 33
110 | RSpec/MultipleExpectations:
111 | Max: 5
112 |
113 | # Offense count: 2
114 | # Configuration parameters: AllowSubject.
115 | RSpec/MultipleMemoizedHelpers:
116 | Max: 6
117 |
118 | # Offense count: 7
119 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples.
120 | # SupportedStyles: always, named_only
121 | RSpec/NamedSubject:
122 | Exclude:
123 | - 'spec/models/calculator/digital_delivery_spec.rb'
124 | - 'spec/models/spree/stock/splitter/digital_splitter_spec.rb'
125 |
126 | # Offense count: 3
127 | # Configuration parameters: AllowedGroups.
128 | RSpec/NestedGroups:
129 | Max: 4
130 |
131 | # Offense count: 4
132 | RSpec/StubbedMock:
133 | Exclude:
134 | - 'spec/controllers/digitals_controller_spec.rb'
135 | - 'spec/models/calculator/digital_delivery_spec.rb'
136 | - 'spec/models/spree/order_spec.rb'
137 |
138 | # Offense count: 1
139 | RSpec/SubjectStub:
140 | Exclude:
141 | - 'spec/models/calculator/digital_delivery_spec.rb'
142 |
143 | # Offense count: 3
144 | # Configuration parameters: ForbiddenMethods, AllowedMethods.
145 | # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
146 | Rails/SkipsModelValidations:
147 | Exclude:
148 | - 'app/models/spree/digital_link.rb'
149 |
150 | # Offense count: 2
151 | # This cop supports unsafe autocorrection (--autocorrect-all).
152 | # Configuration parameters: EnforcedStyle.
153 | # SupportedStyles: nested, compact
154 | Style/ClassAndModuleChildren:
155 | Exclude:
156 | - 'app/models/spree/calculator/shipping/digital_delivery.rb'
157 | - 'app/services/spree/digital_links_creator.rb'
158 |
159 | # Offense count: 1
160 | # This cop supports safe autocorrection (--autocorrect).
161 | # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
162 | Style/GuardClause:
163 | Exclude:
164 | - 'spec/models/spree/drm_record_spec.rb'
165 |
166 | # Offense count: 1
167 | # This cop supports unsafe autocorrection (--autocorrect-all).
168 | # Configuration parameters: Mode.
169 | Style/StringConcatenation:
170 | Exclude:
171 | - 'spec/support/testing_support.rb'
172 |
173 | # Offense count: 2
174 | # This cop supports safe autocorrection (--autocorrect).
175 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
176 | # URISchemes: http, https
177 | Layout/LineLength:
178 | Max: 173
179 |
--------------------------------------------------------------------------------
/spec/models/spree/order_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'spree/testing_support/order_walkthrough'
5 |
6 | RSpec.describe Spree::Order do
7 | context "contents.add" do
8 | let(:order) { create(:order) }
9 |
10 | it "adds digital Variants of quantity 1 to an order" do
11 | variants = 3.times.map { create(:variant, digitals: [create(:digital)]) }
12 | variants.each { |v| order.contents.add(v, 1) }
13 |
14 | expect(order.line_items.first.variant).to eq(variants[0])
15 | expect(order.line_items.second.variant).to eq(variants[1])
16 | expect(order.line_items.third.variant).to eq(variants[2])
17 | end
18 |
19 | it "handles quantity higher than 1 when adding one specific digital Variant" do
20 | digital_variant = create(:variant, digitals: [create(:digital)])
21 | order.contents.add digital_variant, 3
22 | expect(order.line_items.first.quantity).to eq(3)
23 |
24 | order.contents.add digital_variant, 2
25 | expect(order.line_items.first.quantity).to eq(5)
26 | end
27 | end
28 |
29 | context "line_item analysis" do
30 | let(:order) { create(:order) }
31 |
32 | it "understands that all products are digital" do
33 | 3.times do
34 | order.contents.add create(:variant, digitals: [create(:digital)]), 1
35 | end
36 | expect(order.digital?).to be true
37 |
38 | order.contents.add create(:variant, digitals: [create(:digital)]), 4
39 | expect(order.digital?).to be true
40 | end
41 |
42 | it "understands that not all products are digital" do
43 | 3.times do
44 | order.contents.add create(:variant, digitals: [create(:digital)]), 1
45 | end
46 | order.contents.add create(:variant), 1 # this is the analog product
47 | expect(order.digital?).to be false
48 |
49 | order.contents.add create(:variant, digitals: [create(:digital)]), 4
50 | expect(order.digital?).to be false
51 | end
52 | end
53 |
54 | describe '#digital?/#some_digital?' do
55 | let(:order) { create(:order) }
56 | let(:digital_order) {
57 | variants = 3.times.map { create(:variant, digitals: [create(:digital)]) }
58 | variants.each { |v| order.contents.add(v, 1) }
59 | order
60 | }
61 |
62 | let(:mixed_order) {
63 | variants = 2.times.map { create(:variant, digitals: [create(:digital)]) }
64 | variants << create(:variant)
65 | variants.each { |v| order.contents.add(v, 1) }
66 | order
67 | }
68 |
69 | let(:non_digital_order) {
70 | variants = 3.times.map { create(:variant) }
71 | variants.each { |v| order.contents.add(v, 1) }
72 | order
73 | }
74 |
75 | it 'returns true/true for a digital order' do
76 | expect(digital_order).to be_digital
77 | expect(digital_order).to be_some_digital
78 | end
79 |
80 | it 'returns false/true for a mixed order' do
81 | expect(mixed_order).not_to be_digital
82 | expect(mixed_order).to be_some_digital
83 | end
84 |
85 | it 'returns false/false for an exclusively non-digital order' do
86 | expect(non_digital_order).not_to be_digital
87 | expect(non_digital_order).not_to be_some_digital
88 | end
89 | end
90 |
91 | describe '#digital_line_items' do
92 | let(:order) { create(:order) }
93 | let(:digital_order_digitals) { 3.times.map { create(:digital) } }
94 | let(:digital_order) {
95 | variants = digital_order_digitals.map { |d| create(:variant, digitals: [d]) }
96 | variants.each { |v| order.contents.add(v, 1) }
97 | order
98 | }
99 |
100 | let(:mixed_order_digitals) { 2.times.map { create(:digital) } }
101 | let(:mixed_order) {
102 | variants = mixed_order_digitals.map { |d| create(:variant, digitals: [d]) }
103 | variants << create(:variant)
104 | variants.each { |v| order.contents.add(v, 1) }
105 | order
106 | }
107 |
108 | let(:non_digital_order) {
109 | variants = 3.times.map { create(:variant) }
110 | variants.each { |v| order.contents.add(v, 1) }
111 | order
112 | }
113 |
114 | it 'returns true/true for a digital order' do
115 | digital_order_digital_line_items = digital_order.digital_line_items
116 | expect(digital_order_digital_line_items.size).to eq(digital_order_digitals.size)
117 |
118 | variants = digital_order_digital_line_items.map(&:variant)
119 | variants.each { |variant| expect(variant).to be_digital }
120 | digital_order_digitals.each { |d| expect(variants).to include(d.variant) }
121 | end
122 |
123 | it 'returns false/true for a mixed order' do
124 | mixed_order_digital_line_items = mixed_order.digital_line_items
125 | expect(mixed_order_digital_line_items.size).to eq(mixed_order_digitals.size)
126 |
127 | variants = mixed_order_digital_line_items.map(&:variant)
128 | variants.each { |variant| expect(variant).to be_digital }
129 | mixed_order_digitals.each { |d| expect(variants).to include(d.variant) }
130 | end
131 |
132 | it 'returns an empty set for an exclusively non-digital order' do
133 | expect(non_digital_order.digital_line_items).to be_empty
134 | end
135 | end
136 |
137 | describe '#digital_links' do
138 | let(:mixed_order_digitals) { 2.times.map { create(:digital) } }
139 | let(:mixed_order) {
140 | order = create(:order)
141 | variants = mixed_order_digitals.map { |d| create(:variant, digitals: [d]) }
142 | variants << create(:variant)
143 | variants.each { |v| order.contents.add(v, 1) }
144 | order
145 | }
146 |
147 | it 'correctly loads the links' do
148 | mixed_order_digital_links = mixed_order.digital_links
149 | links_from_digitals = mixed_order_digitals.map(&:reload).map(&:digital_links).flatten
150 | expect(mixed_order_digital_links.size).to eq(links_from_digitals.size)
151 |
152 | mixed_order_digital_links.each do |l|
153 | expect(links_from_digitals).to include(l)
154 | end
155 | end
156 | end
157 |
158 | describe '#generate_digital_links' do
159 | let(:digital_variant) { create(:variant, digitals: [create(:digital)]) }
160 |
161 | subject(:links) { order.digital_links }
162 |
163 | context "when order in complete state" do
164 | let(:order) {
165 | create(
166 | :order_ready_to_complete,
167 | line_items_attributes: [{ variant: digital_variant, quantity: 8 }]
168 | )
169 | }
170 |
171 | it "creates a link for each quantity of a digital Variant" do
172 | order.complete!
173 | expect(links.count).to eq(8)
174 | end
175 | end
176 |
177 | context "when order is in other state" do
178 | let(:order) {
179 | create(
180 | :order_with_line_items,
181 | line_items_attributes: [{ variant: digital_variant, quantity: 8 }]
182 | )
183 | }
184 |
185 | it "doesn't create a link for digital Variant" do
186 | expect(links.count).to eq(0)
187 | end
188 | end
189 | end
190 |
191 | describe '#reset_digital_links!' do
192 | let!(:order) { build(:order) }
193 | let!(:link_1) { double }
194 | let!(:link_2) { double }
195 |
196 | before do
197 | expect(link_1).to receive(:reset!)
198 | expect(link_2).to receive(:reset!)
199 | expect(order).to receive(:digital_links).and_return([link_1, link_2])
200 | end
201 |
202 | it 'calls reset on the links' do
203 | order.reset_digital_links!
204 | end
205 | end
206 | end
207 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # solidus_digital
2 |
3 | [](https://circleci.com/gh/solidusio-contrib/solidus_digital)
4 |
5 | This is a [Solidus](http://solidus.io/) extension to enable downloadable products (ebooks, MP3s, videos, etc).
6 |
7 | This documentation is not complete and possibly out of date in some cases.
8 | There are features that have been implemented that are not documented here, please look at the source for complete documentation.
9 |
10 | ### Digital products
11 | The idea is simple.
12 | 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.
13 | There are a few assumptions that solidus_digital (currently) makes and it's important to be aware of them.
14 |
15 | * The table structure of spree_core is not touched.
16 | Spree digital lives parallel to spree_core and does change the existing database, except adding two new tables.
17 | * The download links will be sent via email in the order confirmation (or "resend" from the admin section).
18 | The links do *not* appear in the order "overview" that the customer sees.
19 | Adding download buttons to `OrdersController#show` is easy, [check out this gist](https://gist.github.com/3187793#file_add_solidus_digital_buttons_to_invoice.rb).
20 | * Once the order is checked-out, the download links will immediately be sent (i.e. in the order confirmation).
21 | You'll have to modify the system to support 'delayed' payments (like a billable account).
22 | * You should create a ShippingMethod based on the Digital Delivery calculator type.
23 | 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).
24 | Checkout the [source code](https://github.com/halo/solidus_digital/blob/master/app/models/spree/calculator/digital_delivery.rb) for the Digital Delivery calculator for more information.
25 | * One may buy several items of the same digital product in one cart.
26 | The customer will simply receive several links by doing so.
27 | This allows customer's to legally purchase multiple copies of the same product and maybe give one away to a friend.
28 | * You can set how many times (clicks) the users downloads will work.
29 | You can also set how long the users links will work (expiration).
30 | For more information, [check out the preferences object](https://github.com/halo/solidus_digital/blob/master/lib/spree/solidus_digital_configuration.rb)
31 | * The file `views/order_mailer/confirm_email.text.erb` needs to be customized by you.
32 | If you are looking for HTML emails, [this branch of spree-html-email](http://github.com/iloveitaly/spree-html-email) supports solidus_digital.
33 | * A purchased product can be downloaded even if you disable the product immediately.
34 | You would have to remove the attached file in your admin section to prevent people from downloading purchased products.
35 | * File are uploaded to `RAILS_ROOT/private`.
36 | Make sure it's symlinked in case you're using Capistrano.
37 | If you want to change the upload path, [check out this gist](https://gist.github.com/3187793#file_solidus_digital_path_change_decorator.rb).
38 | * 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
39 | * We use send_file to send the files on download.
40 | See below for instructions on how to push file downloading off to nginx.
41 |
42 | ## Issues
43 |
44 | Current version of `solidus_digital` is not compatable with `solidus` version 2.3.0.
45 |
46 | ## Quickstart
47 |
48 | Add this line to the `Gemfile` in your Spree project:
49 |
50 | ```ruby
51 | gem 'solidus_digital', github: 'solidusio-contrib/solidus_digital'
52 | ```
53 |
54 | The following terminal commands will copy the migration files to the corresponding directory in your Rails application and apply the migrations to your database.
55 |
56 | ```shell
57 | bundle exec rails g solidus_digital:install
58 | bundle exec rake db:migrate
59 | ```
60 |
61 | Then set any preferences in the web interface.
62 |
63 | ### Shipping Configuration
64 |
65 | You should create a ShippingMethod based on the Digital Delivery calculator type.
66 | It will be detected by `solidus_digital`.
67 | Otherwise your customer will be forced to choose something like "UPS" even if they purchase only downloadable products.
68 |
69 | ### Links access configuration
70 |
71 | Configuration class `Spree::DigitalConfiguration`.
72 | Default configuration:
73 |
74 | ```ruby
75 | class SpreeDigitalConfiguration < Preferences::Configuration
76 | # number of times a customer can download a digital file
77 | # nil - infinite number of clicks
78 | preference :authorized_clicks, :integer, default: 3
79 |
80 | # number of days after initial purchase the customer can download a file
81 | preference :authorized_days, :integer, default: 2
82 |
83 | # should digitals be kept around after the associated product is destroyed
84 | preference :keep_digitals, :boolean, default: false
85 |
86 | # number of seconds before an s3 link expires
87 | preference :s3_expiration_seconds, :integer, default: 10
88 |
89 | # number of digital links generated per line item
90 | # accepts: 'quantity' or Integer numbers
91 | # quantity - 'line_item.quantity' digital links will be generated
92 | # Integer 'number' - 'number' digital links will be generated
93 | preference :digital_links_count, :string, default: 'quantity'
94 | end
95 |
96 | ```
97 |
98 | Example:
99 | ```ruby
100 | Rails.application.config.after_initialize do
101 | Spree::DigitalConfiguration[:authorized_clicks] = nil # infinite access for user
102 | end
103 | ```
104 |
105 | ### DRM
106 |
107 | If you want to create attachment with [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) for your digital product, e.g.: _watermark_ or _digital signature_,
108 | you'll need to implement a class which will transform original attachement from `Spree::Digital` class, to modified attachment. This attachment will be stored as `Spree::DrmRecord` which is assigned to `Spree::Digital` class. And check "DRM" checkbox while creating Digital for product.
109 |
110 | For example:
111 | ```ruby
112 | class SampleDrmMaker
113 | def initialize(drm_record)
114 | @drm_record = drm_record
115 | end
116 |
117 | def create!
118 | # DRM file attachment specific code
119 | @drm_record.attachment = drm_attachemnt
120 | end
121 | end
122 | ```
123 |
124 | Then insert it into `DrmClass`:
125 | ```ruby
126 |
127 | Spree::DrmRecord.class_eval do
128 | private
129 | def prepare_drm_mark
130 | SampleDrmMaker.new(self).create!
131 | end
132 | end
133 | ```
134 |
135 | `prepare_drm_mark` method will call **after_create** for `Spree::DrmRecord`. We'd suggest to run your drm maker class in parallel with [Delayed::Job](https://github.com/collectiveidea/delayed_job) or [Sidekiq](https://github.com/mperham/sidekiq).
136 |
137 | Every time user confirms order on checkout process, new `Spree::DrmRecord` will be created for every `LineItem` which has digital product with enabled `DRM` flag.
138 |
139 |
140 | ### Improving File Downloading: `send_file` + nginx
141 |
142 | Without customization, all file downloading will route through the rails stack.
143 | 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.
144 |
145 | Luckily there is an easy way around this:
146 | pass off file downloading to nginx (or apache, etc).
147 | Take a look at [this article](http://blog.kiskolabs.com/post/637725747/nginx-rails-send-file) for a good explanation.
148 |
149 | ```ruby
150 | # in your app's source
151 | # config/environments/production.rb
152 |
153 | # Specifies the header that your server uses for sending files
154 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
155 | config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
156 | ```
157 |
158 | ```nginx
159 | # on your server
160 | # e.g. /etc/nginx/sites-available/spree-secure
161 | upstream unicorn_spree_secure {
162 | server unix:/data/spree/shared/sockets/unicorn.sock fail_timeout=0;
163 | }
164 | server {
165 | listen 443;
166 | ...
167 |
168 | location / {
169 | proxy_set_header X_FORWARDED_PROTO https;
170 | ...
171 | proxy_set_header X-Sendfile-Type X-Accel-Redirect;
172 | proxy_set_header X-Accel-Mapping /data/spree/shared/uploaded-files/digitals/=/digitals/;
173 | ...
174 | }
175 |
176 | location /digitals/ {
177 | internal;
178 | root /data/spree/shared/uploaded-files/;
179 | }
180 | ...
181 | }
182 | ```
183 |
184 | References:
185 |
186 | * [Gist of example config](https://gist.github.com/416004)
187 | * [Change paperclip's upload / download path](https://gist.github.com/3187793#file_solidus_digital_path_change_decorator.rb)
188 | * ["X-Accel-Mapping header missing" in nginx error log](http://stackoverflow.com/questions/6237016/message-x-accel-mapping-header-missing-in-nginx-error-log)
189 | * [Another good, but older, explanation](http://kovyrin.net/2006/11/01/nginx-x-accel-redirect-php-rails/)
190 |
191 | ### Development
192 |
193 | #### Table Diagram
194 |
195 |
196 |
197 | #### Testing
198 |
199 | ```shell
200 | rake test_app
201 | rake rspec
202 | ```
203 |
204 | ### Contributors
205 |
206 | See https://github.com/solidusio-contrib/solidus_digital/graphs/contributors
207 |
208 | ### License
209 |
210 | MIT © 2011-2015 halo, see [LICENSE](https://github.com/solidusio-contrib/solidus_digital/blob/master/LICENSE.md)
211 |
--------------------------------------------------------------------------------